local _, addonTable = ...
local WipedQoL = addonTable.WipedQoL
local UI = addonTable.UI
local TRANSLATE = addonTable.TRANSLATE or function(msg) return msg end

local REFRESH_ICON = "Interface\\AddOns\\WipedQoL\\Icons\\refresh"
local UPGRADE_ICON = "Interface\\AddOns\\WipedQoL\\Icons\\potionchange"
local MACRO_TOOLTIP_ICON_SIZE = 30
local MACRO_TOOLTIP_ICON_GAP = 4
local CARD_COLUMNS = 4
local CARD_WIDTH = 56
local CARD_HEIGHT = 72
local CARD_GAP = 8
local CARD_ROW_GAP = 4
local PREVIEW_ONE_ROW_HEIGHT = 140
local CARD_ICON_SIZE = 44
local DEFAULT_MACRO_ICON = "Interface\\Icons\\INV_Misc_QuestionMark"

local MODIFIER_OPTIONS = {
    { value = "SHIFT", label = "Shift" },
    { value = "CTRL", label = "Ctrl" },
    { value = "ALT", label = "Alt" },
}

local function CreateIconButton(parent, iconPath)
    local button = CreateFrame("Button", nil, parent)
    button:SetSize(24, 24)
    UI.SkinButton(button)

    local icon = button:CreateTexture(nil, "ARTWORK")
    icon:SetPoint("TOPLEFT", button, "TOPLEFT", 5, -5)
    icon:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", -5, 5)
    icon:SetTexture(iconPath)
    button.icon = icon

    return button
end

local function GetMacroIcon(icon)
    if not icon or icon == 0 or icon == "" then
        return DEFAULT_MACRO_ICON
    end

    return icon
end

local function GetPotionDetailText(item, itemID)
    item = item or {}
    if type(item.variants) == "table" and #item.variants > 0 then
        local lines = {}
        for _, variant in ipairs(item.variants) do
            local detail = GetPotionDetailText(variant)
            if detail ~= "" then
                lines[#lines + 1] = detail
            end
        end
        return table.concat(lines, "\n")
    end

    itemID = tonumber(itemID or item.id)
    if not itemID then
        return ""
    end

    local qualityLabels = {
        bronze = "Bronze",
        silver = "Silver",
        gold = "Gold",
    }
    local qualityLabel = qualityLabels[item.quality]
    if qualityLabel then
        return string.format("%s - ID: %d", TRANSLATE(qualityLabel), itemID)
    end
    return string.format("ID: %d", itemID)
end

local function CreatePotionQualityBadge(parent, icon)
    local badge = parent:CreateTexture(nil, "OVERLAY", nil, 7)
    badge:SetSize(28, 24)
    badge:SetPoint("TOPLEFT", icon, "TOPLEFT", -5, 5)
    badge:Hide()
    return badge
end

local function SetPotionQualityBadge(badge, item)
    local atlas = item and item.qualityAtlas
    if badge and atlas and badge.SetAtlas then
        local ok = pcall(badge.SetAtlas, badge, atlas, false)
        badge:SetShown(ok)
        return
    end
    if badge then
        badge:Hide()
    end
end

local function SetPotionQualityBadges(primaryBadge, secondaryBadge, item)
    local variants = item and item.variants
    if type(variants) == "table" and #variants > 0 then
        SetPotionQualityBadge(primaryBadge, variants[1])
        SetPotionQualityBadge(secondaryBadge, variants[2])
        return
    end

    SetPotionQualityBadge(primaryBadge, item)
    SetPotionQualityBadge(secondaryBadge, nil)
end

local function SetButtonHoverBorder(button, r, g, b, a)
    local hover = button and button._rhhHover
    if type(hover) == "table" then
        for _, texture in pairs(hover) do
            texture:SetVertexColor(r or 0, g or 0, b or 0, a or 0)
        end
    elseif hover and hover.SetVertexColor then
        hover:SetVertexColor(r or 0, g or 0, b or 0, a or 0)
    elseif button and button.SetBackdropBorderColor then
        button:SetBackdropBorderColor(r or 0, g or 0, b or 0, a or 0)
    end
end

local function CreateMacroCard(parent)
    local card = CreateFrame("Button", nil, parent, "BackdropTemplate")
    card:SetSize(CARD_WIDTH, CARD_HEIGHT)
    card:RegisterForDrag("LeftButton")
    UI.SkinFrame(card, { light = true })

    local iconInset = 2
    local function SetIconInset(inset)
        iconInset = inset
        card.icon:ClearAllPoints()
        card.icon:SetPoint("TOPLEFT", card.iconBox, "TOPLEFT", inset, -inset)
        card.icon:SetPoint("BOTTOMRIGHT", card.iconBox, "BOTTOMRIGHT", -inset, inset)
    end

    local function AnimateIconZoom(targetInset)
        local startInset = iconInset or 2
        local elapsedTime = 0

        card:SetScript("OnUpdate", function(self, delta)
            elapsedTime = elapsedTime + delta
            local progress = math.min(elapsedTime / 0.12, 1)
            local eased = 1 - ((1 - progress) * (1 - progress))
            SetIconInset(startInset + ((targetInset - startInset) * eased))

            if progress >= 1 then
                self:SetScript("OnUpdate", nil)
                SetIconInset(targetInset)
            end
        end)
    end

    card.iconBox = CreateFrame("Frame", nil, card)
    card.iconBox:SetPoint("TOP", card, "TOP", 0, -4)
    card.iconBox:SetSize(CARD_ICON_SIZE, CARD_ICON_SIZE)

    card.icon = card:CreateTexture(nil, "ARTWORK")
    card.icon:SetPoint("TOPLEFT", card.iconBox, "TOPLEFT", iconInset, -iconInset)
    card.icon:SetPoint("BOTTOMRIGHT", card.iconBox, "BOTTOMRIGHT", -iconInset, iconInset)
    card.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    card.dim = card:CreateTexture(nil, "OVERLAY", nil, 1)
    card.dim:SetAllPoints(card.icon)
    card.dim:SetTexture("Interface\\Buttons\\WHITE8X8")

    card.iconButton = CreateFrame("Button", nil, card)
    card.iconButton:SetAllPoints(card.iconBox)
    card.iconButton:RegisterForDrag("LeftButton")

    card.upgradeButton = CreateFrame("Button", nil, card, "BackdropTemplate")
    card.upgradeButton:SetSize(18, 18)
    card.upgradeButton:SetPoint("TOPRIGHT", card.iconBox, "TOPRIGHT", 4, 4)
    card.upgradeButton:SetFrameLevel(card.iconButton:GetFrameLevel() + 5)
    UI.SkinButton(card.upgradeButton)
    card.upgradeButton.icon = card.upgradeButton:CreateTexture(nil, "ARTWORK")
    card.upgradeButton.icon:SetPoint("TOPLEFT", card.upgradeButton, "TOPLEFT", 3, -3)
    card.upgradeButton.icon:SetPoint("BOTTOMRIGHT", card.upgradeButton, "BOTTOMRIGHT", -3, 3)
    card.upgradeButton.icon:SetTexture(UPGRADE_ICON)
    card.upgradeButton:Hide()

    card.border = {}
    card.border.top = card:CreateTexture(nil, "OVERLAY", nil, 3)
    card.border.top:SetPoint("TOPLEFT", card.iconBox, "TOPLEFT", 0, 0)
    card.border.top:SetPoint("TOPRIGHT", card.iconBox, "TOPRIGHT", 0, 0)
    card.border.top:SetHeight(1)
    card.border.bottom = card:CreateTexture(nil, "OVERLAY", nil, 3)
    card.border.bottom:SetPoint("BOTTOMLEFT", card.iconBox, "BOTTOMLEFT", 0, 0)
    card.border.bottom:SetPoint("BOTTOMRIGHT", card.iconBox, "BOTTOMRIGHT", 0, 0)
    card.border.bottom:SetHeight(1)
    card.border.left = card:CreateTexture(nil, "OVERLAY", nil, 3)
    card.border.left:SetPoint("TOPLEFT", card.iconBox, "TOPLEFT", 0, 0)
    card.border.left:SetPoint("BOTTOMLEFT", card.iconBox, "BOTTOMLEFT", 0, 0)
    card.border.left:SetWidth(1)
    card.border.right = card:CreateTexture(nil, "OVERLAY", nil, 3)
    card.border.right:SetPoint("TOPRIGHT", card.iconBox, "TOPRIGHT", 0, 0)
    card.border.right:SetPoint("BOTTOMRIGHT", card.iconBox, "BOTTOMRIGHT", 0, 0)
    card.border.right:SetWidth(1)
    for _, texture in pairs(card.border) do
        texture:SetColorTexture(1, 1, 1, 0.18)
    end

    card.nameBg = card:CreateTexture(nil, "OVERLAY", nil, 2)
    card.nameBg:SetPoint("BOTTOMLEFT", card.iconBox, "BOTTOMLEFT", 0, 0)
    card.nameBg:SetPoint("BOTTOMRIGHT", card.iconBox, "BOTTOMRIGHT", 0, 0)
    card.nameBg:SetHeight(16)
    card.nameBg:SetTexture("Interface\\Buttons\\WHITE8X8")
    card.nameBg:SetVertexColor(0, 0, 0, 0.82)

    card.name = card:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    card.name:SetPoint("LEFT", card.nameBg, "LEFT", 3, 0)
    card.name:SetPoint("RIGHT", card.nameBg, "RIGHT", -3, 0)
    card.name:SetJustifyH("CENTER")
    card.name:SetTextColor(1, 1, 1, 0.96)
    card.name:SetWordWrap(false)
    UI.ApplyFont(card.name, 9, "OUTLINE")

    card.toggle = UI.CreateToggleSwitch(card, 28, 14)
    card.toggle:SetPoint("TOP", card.iconBox, "BOTTOM", 0, -4)
    card.AnimateIconZoom = AnimateIconZoom
    card.SetIconBorderColor = function(self, r, g, b, a)
        for _, texture in pairs(self.border) do
            texture:SetColorTexture(r, g, b, a)
        end
    end

    return card
end

local macroPotionTooltip

local function EnsureMacroPotionTooltip()
    if macroPotionTooltip then
        return macroPotionTooltip
    end

    local tooltip = CreateFrame(
        "Frame",
        "WipedQoLCombatPotionMacroTooltip",
        UIParent,
        "BackdropTemplate"
    )
    tooltip:SetFrameStrata("TOOLTIP")
    tooltip:SetClampedToScreen(true)
    tooltip:EnableMouse(false)
    UI.SkinFrame(tooltip)

    tooltip.title = UI.CreateLabel(tooltip, "", 12)
    tooltip.title:SetPoint("TOPLEFT", tooltip, "TOPLEFT", 10, -9)
    tooltip.title:SetTextColor(1, 1, 1)

    tooltip.state = UI.CreateLabel(tooltip, "", 10)
    tooltip.state:SetPoint("TOPLEFT", tooltip.title, "BOTTOMLEFT", 0, -3)

    tooltip.hint = UI.CreateLabel(tooltip, TRANSLATE("Drag onto actionbar."), 10)
    tooltip.hint:SetTextColor(0.72, 0.72, 0.72)

    tooltip.icons = {}
    tooltip:Hide()
    macroPotionTooltip = tooltip
    return tooltip
end

local function EnsureMacroTooltipIcon(tooltip, index)
    local iconFrame = tooltip.icons[index]
    if iconFrame then
        return iconFrame
    end

    iconFrame = CreateFrame("Frame", nil, tooltip)
    iconFrame:SetSize(MACRO_TOOLTIP_ICON_SIZE, MACRO_TOOLTIP_ICON_SIZE)

    iconFrame.icon = iconFrame:CreateTexture(nil, "ARTWORK")
    iconFrame.icon:SetAllPoints(iconFrame)
    iconFrame.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    iconFrame.quality = CreatePotionQualityBadge(iconFrame, iconFrame.icon)
    iconFrame.quality:SetSize(22, 19)
    iconFrame.quality:ClearAllPoints()
    iconFrame.quality:SetPoint("TOPLEFT", iconFrame.icon, "TOPLEFT", -4, 4)

    tooltip.icons[index] = iconFrame
    return iconFrame
end

local function HideMacroTooltip()
    if macroPotionTooltip then
        macroPotionTooltip:Hide()
    end
end

local function ShowMacroTooltip(owner, macro)
    macro = macro or {}
    GameTooltip:Hide()

    local tooltip = EnsureMacroPotionTooltip()
    tooltip.title:SetText(macro.name or TRANSLATE("Unnamed macro"))
    tooltip.state:SetText(macro.paused and TRANSLATE("paused") or TRANSLATE("active"))
    tooltip.state:SetTextColor(
        macro.paused and 0.9 or 0.2,
        macro.paused and 0.22 or 0.85,
        macro.paused and 0.18 or 0.36
    )

    local catalogByID = {}
    if WipedQoL.GetCombatPotionCatalog then
        for _, item in ipairs(WipedQoL:GetCombatPotionCatalog()) do
            catalogByID[item.id] = item
        end
    end

    local potions = macro.combatPotions
        or (macro.combatPotion and { macro.combatPotion })
        or {}
    for _, iconFrame in ipairs(tooltip.icons) do
        iconFrame:Hide()
    end

    local iconSize = MACRO_TOOLTIP_ICON_SIZE
    local iconGap = MACRO_TOOLTIP_ICON_GAP
    for index, potion in ipairs(potions) do
        local iconFrame = EnsureMacroTooltipIcon(tooltip, index)
        local item = catalogByID[tonumber(potion.id)]
        iconFrame.icon:SetTexture(
            item and item.icon
                or potion.icon
                or DEFAULT_MACRO_ICON
        )
        SetPotionQualityBadge(iconFrame.quality, item)
        iconFrame:ClearAllPoints()
        iconFrame:SetPoint(
            "TOPLEFT",
            tooltip.state,
            "BOTTOMLEFT",
            (index - 1) * (iconSize + iconGap),
            -7
        )
        iconFrame:Show()
    end

    local iconRowWidth = math.max(
        (#potions * iconSize) + (math.max(#potions - 1, 0) * iconGap),
        iconSize
    )
    tooltip.hint:ClearAllPoints()
    tooltip.hint:SetPoint("TOPLEFT", tooltip.state, "BOTTOMLEFT", 0, -44)

    local contentWidth = math.max(
        iconRowWidth,
        tooltip.title:GetStringWidth() or 0,
        tooltip.hint:GetStringWidth() or 0
    )
    tooltip:SetSize(math.max(contentWidth + 20, 170), 104)
    tooltip:ClearAllPoints()
    tooltip:SetPoint("TOPLEFT", owner, "TOPRIGHT", 8, 0)
    tooltip:Show()
end

local function CreateHealthMacroButton(parent)
    local button = CreateFrame("Button", nil, parent)
    button:SetSize(42, 42)
    button:RegisterForDrag("LeftButton")

    button.icon = button:CreateTexture(nil, "ARTWORK")
    button.icon:SetPoint("TOPLEFT", button, "TOPLEFT", 2, -2)
    button.icon:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", -2, 2)
    button.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    local iconInset = 2
    local function SetIconInset(inset)
        iconInset = inset
        button.icon:ClearAllPoints()
        button.icon:SetPoint("TOPLEFT", button, "TOPLEFT", inset, -inset)
        button.icon:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", -inset, inset)
    end

    local function AnimateIconZoom(targetInset)
        local startInset = iconInset or 2
        local elapsedTime = 0

        button:SetScript("OnUpdate", function(self, delta)
            elapsedTime = elapsedTime + delta
            local progress = math.min(elapsedTime / 0.12, 1)
            local eased = 1 - ((1 - progress) * (1 - progress))
            SetIconInset(startInset + ((targetInset - startInset) * eased))

            if progress >= 1 then
                self:SetScript("OnUpdate", nil)
                SetIconInset(targetInset)
            end
        end)
    end

    button.border = {}
    button.border.top = button:CreateTexture(nil, "OVERLAY")
    button.border.top:SetPoint("TOPLEFT", button, "TOPLEFT", 0, 0)
    button.border.top:SetPoint("TOPRIGHT", button, "TOPRIGHT", 0, 0)
    button.border.top:SetHeight(1)
    button.border.bottom = button:CreateTexture(nil, "OVERLAY")
    button.border.bottom:SetPoint("BOTTOMLEFT", button, "BOTTOMLEFT", 0, 0)
    button.border.bottom:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 0, 0)
    button.border.bottom:SetHeight(1)
    button.border.left = button:CreateTexture(nil, "OVERLAY")
    button.border.left:SetPoint("TOPLEFT", button, "TOPLEFT", 0, 0)
    button.border.left:SetPoint("BOTTOMLEFT", button, "BOTTOMLEFT", 0, 0)
    button.border.left:SetWidth(1)
    button.border.right = button:CreateTexture(nil, "OVERLAY")
    button.border.right:SetPoint("TOPRIGHT", button, "TOPRIGHT", 0, 0)
    button.border.right:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 0, 0)
    button.border.right:SetWidth(1)
    for _, texture in pairs(button.border) do
        texture:SetColorTexture(1, 1, 1, 0.18)
    end

    button.highlight = button:CreateTexture(nil, "HIGHLIGHT")
    button.highlight:SetAllPoints(button.icon)
    button.highlight:SetColorTexture(1, 1, 1, 0)

    button.countText = button:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    button.countText:SetPoint("BOTTOMRIGHT", button.icon, "BOTTOMRIGHT", 2, -2)
    button.countText:SetJustifyH("RIGHT")
    button.countText:SetTextColor(1, 1, 1, 1)
    button.countText:SetShadowOffset(1, -1)
    button.countText:SetShadowColor(0, 0, 0, 1)
    UI.ApplyFont(button.countText, 10, "OUTLINE")

    button.AnimateIconZoom = AnimateIconZoom

    return button
end

local function EnsureHealthSpellEditor(addon)
    if addon.healthSpellEditor then
        return addon.healthSpellEditor
    end

    local editor = CreateFrame("Frame", "WipedQoLHealthSpellEditor", addon.mainWindow or UIParent, "BackdropTemplate")
    editor:SetSize(420, 360)
    editor:SetPoint("CENTER", UIParent, "CENTER")
    editor:SetFrameStrata("DIALOG")
    editor:SetFrameLevel((addon.mainWindow and addon.mainWindow:GetFrameLevel() or 0) + 50)
    editor:SetMovable(true)
    editor:EnableMouse(true)
    editor:RegisterForDrag("LeftButton")
    editor:SetScript("OnDragStart", editor.StartMoving)
    editor:SetScript("OnDragStop", editor.StopMovingOrSizing)
    UI.SkinFrame(editor)

    local title = UI.CreateLabel(editor, TRANSLATE("Class healing spells"), 14)
    title:SetPoint("TOPLEFT", editor, "TOPLEFT", 14, -13)
    title:SetTextColor(1, 1, 1)

    local closeButton = CreateFrame("Button", nil, editor, "UIPanelCloseButton")
    closeButton:SetPoint("TOPRIGHT", editor, "TOPRIGHT", -5, -5)
    closeButton:SetScript("OnClick", function() editor:Hide() end)
    UI.SkinCloseButton(closeButton)

    editor.classLabel = UI.CreateLabel(editor, "", 11)
    editor.classLabel:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -8)
    editor.classLabel:SetTextColor(0.72, 0.78, 0.88)

    editor.lengthLabel = UI.CreateLabel(editor, "", 11)
    editor.lengthLabel:SetPoint("TOPRIGHT", editor, "TOPRIGHT", -14, -43)
    editor.lengthLabel:SetJustifyH("RIGHT")

    local input = CreateFrame("EditBox", nil, editor, "InputBoxTemplate")
    input:SetSize(230, 26)
    input:SetPoint("TOPLEFT", editor.classLabel, "BOTTOMLEFT", 0, -10)
    input:SetAutoFocus(false)
    input:SetNumeric(true)
    input:SetMaxLetters(10)
    input:SetTextInsets(8, 8, 0, 0)
    UI.ApplyFont(input, 12, "")
    if input.Left then input.Left:Hide() end
    if input.Middle then input.Middle:Hide() end
    if input.Right then input.Right:Hide() end
    UI.SkinFrame(input, { light = true })

    local addButton = CreateFrame("Button", nil, editor, "UIPanelButtonTemplate")
    addButton:SetSize(116, 26)
    addButton:SetPoint("LEFT", input, "RIGHT", 8, 0)
    addButton:SetText(TRANSLATE("Add Spell ID"))
    UI.SkinButton(addButton)

    editor.statusLabel = UI.CreateLabel(editor, "", 10)
    editor.statusLabel:SetPoint("TOPLEFT", input, "BOTTOMLEFT", 0, -5)
    editor.statusLabel:SetPoint("RIGHT", editor, "RIGHT", -14, 0)
    editor.statusLabel:SetJustifyH("LEFT")
    editor.statusLabel:SetTextColor(1, 0.35, 0.28)

    local listScroll = CreateFrame("ScrollFrame", nil, editor)
    listScroll:SetPoint("TOPLEFT", editor.statusLabel, "BOTTOMLEFT", 0, -8)
    listScroll:SetPoint("BOTTOMRIGHT", editor, "BOTTOMRIGHT", -30, 14)
    listScroll:EnableMouseWheel(true)
    if listScroll.SetClipsChildren then
        listScroll:SetClipsChildren(true)
    end

    local listChild = CreateFrame("Frame", nil, listScroll)
    listChild:SetSize(1, 1)
    listScroll:SetScrollChild(listChild)

    local scrollbar = CreateFrame("Slider", nil, editor, "BackdropTemplate")
    scrollbar:SetPoint("TOPRIGHT", listScroll, "TOPRIGHT", 18, 0)
    scrollbar:SetPoint("BOTTOMRIGHT", listScroll, "BOTTOMRIGHT", 18, 0)
    scrollbar:SetWidth(10)
    scrollbar:SetOrientation("VERTICAL")
    scrollbar:SetValueStep(34)
    UI.SkinFrame(scrollbar, { light = true })
    scrollbar:SetThumbTexture("Interface\\Buttons\\WHITE8X8")
    local thumb = scrollbar:GetThumbTexture()
    if thumb then
        thumb:SetSize(10, 30)
        UI.SetAddonColor(thumb, "SetVertexColor")
    end
    scrollbar:SetScript("OnValueChanged", function(_, value)
        listScroll:SetVerticalScroll(value or 0)
    end)
    listScroll:SetScript("OnMouseWheel", function(_, delta)
        local minValue, maxValue = scrollbar:GetMinMaxValues()
        scrollbar:SetValue(math.max(minValue, math.min(
            maxValue,
            (scrollbar:GetValue() or 0) - (delta * 34)
        )))
    end)

    editor.rows = {}
    local function EnsureRow(index)
        local row = editor.rows[index]
        if row then
            return row
        end

        row = CreateFrame("Frame", nil, listChild, "BackdropTemplate")
        row:SetHeight(32)
        UI.SkinFrame(row, { light = true })

        row.checkbox = UI.CreateCheckBox(row, "", function()
            for _, entry in ipairs(addon:GetHealthMacroClassSpells()) do
                if entry.id == row.spellID then
                    return entry.enabled == true
                end
            end
            return false
        end, function(value)
            local success, reason = addon:SetHealthMacroClassSpellEnabled(row.spellID, value)
            if not success and reason == "macroTooLong" then
                editor.statusLabel:SetText(TRANSLATE("This spell would exceed the macro length limit."))
            end
        end)
        row.checkbox:SetPoint("LEFT", row, "LEFT", 8, 0)
        row.checkbox:SetSize(310, 28)
        if row.checkbox.label then
            row.checkbox.label:SetWidth(245)
            row.checkbox.label:SetWordWrap(false)
        end

        row.removeButton = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
        row.removeButton:SetSize(28, 24)
        row.removeButton:SetPoint("RIGHT", row, "RIGHT", -4, 0)
        row.removeButton:SetText("×")
        row.removeButton:SetScript("OnClick", function()
            addon:RemoveHealthMacroClassSpell(row.spellID)
        end)
        UI.SkinButton(row.removeButton)

        editor.rows[index] = row
        return row
    end

    function editor:Refresh()
        local localizedClass, classFile = UnitClass("player")
        self.classLabel:SetText(string.format(
            "%s: %s",
            TRANSLATE("Class"),
            localizedClass or classFile or ""
        ))
        self.statusLabel:SetText("")

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

        local entries = addon:GetHealthMacroClassSpells()
        for index, entry in ipairs(entries) do
            local row = EnsureRow(index)
            row.spellID = entry.id
            local spellName = C_Spell and C_Spell.GetSpellName and C_Spell.GetSpellName(entry.id)
                or tostring(entry.id)
            row.checkbox.label:SetText(string.format("%s (%d)", spellName, entry.id))
            row.checkbox:Refresh(false)
            row:ClearAllPoints()
            row:SetPoint("TOPLEFT", listChild, "TOPLEFT", 0, -((index - 1) * 36))
            row:SetPoint("RIGHT", listChild, "RIGHT", 0, 0)
            row:Show()
        end

        local contentHeight = math.max(#entries * 36, 1)
        listChild:SetSize(math.max(listScroll:GetWidth() or 1, 1), contentHeight)
        local maxScroll = math.max(contentHeight - (listScroll:GetHeight() or 0), 0)
        scrollbar:SetMinMaxValues(0, maxScroll)
        scrollbar:SetValue(math.min(scrollbar:GetValue() or 0, maxScroll))
        scrollbar:SetShown(maxScroll > 0)

        local macroLength = #(addon:GetHealthMacroBody() or "")
        local maxLength = addon:GetHealthMacroMaxLength()
        self.lengthLabel:SetText(string.format(
            TRANSLATE("Macro length: %d/%d"),
            macroLength,
            maxLength
        ))
        if macroLength > maxLength then
            self.lengthLabel:SetTextColor(1, 0.2, 0.2)
        elseif macroLength >= maxLength - 25 then
            self.lengthLabel:SetTextColor(1, 0.72, 0.12)
        else
            self.lengthLabel:SetTextColor(0.45, 0.9, 0.52)
        end
    end

    local function AddSpell()
        local success, reason = addon:AddHealthMacroClassSpell(tonumber(input:GetText()))
        if success then
            input:SetText("")
            editor:Refresh()
            if reason == "macroTooLong" then
                editor.statusLabel:SetText(TRANSLATE("This spell would exceed the macro length limit."))
            end
        else
            editor.statusLabel:SetText(TRANSLATE("Invalid or duplicate spell ID."))
        end
    end
    addButton:SetScript("OnClick", AddSpell)
    input:SetScript("OnEnterPressed", function(self)
        AddSpell()
        self:ClearFocus()
    end)
    editor:SetScript("OnShow", function(self) self:Refresh() end)
    editor:Hide()

    addon.healthSpellEditor = editor
    return editor
end

local function EnsureCombatPotionUpgradeDialog(addon)
    if addon.combatPotionUpgradeDialog then
        return addon.combatPotionUpgradeDialog
    end

    local dialog = CreateFrame(
        "Frame",
        "WipedQoLCombatPotionUpgradeDialog",
        addon.mainWindow or UIParent,
        "BackdropTemplate"
    )
    dialog:SetSize(620, 480)
    dialog:SetPoint("CENTER", UIParent, "CENTER")
    dialog:SetFrameStrata("DIALOG")
    dialog:SetFrameLevel((addon.mainWindow and addon.mainWindow:GetFrameLevel() or 0) + 60)
    dialog:SetMovable(true)
    dialog:EnableMouse(true)
    dialog:RegisterForDrag("LeftButton")
    dialog:SetScript("OnDragStart", dialog.StartMoving)
    dialog:SetScript("OnDragStop", dialog.StopMovingOrSizing)
    UI.SkinFrame(dialog)

    local title = UI.CreateLabel(dialog, TRANSLATE("Replace combat potion"), 14)
    title:SetPoint("TOPLEFT", dialog, "TOPLEFT", 14, -13)
    title:SetTextColor(1, 1, 1)

    local closeButton = CreateFrame("Button", nil, dialog, "UIPanelCloseButton")
    closeButton:SetPoint("TOPRIGHT", dialog, "TOPRIGHT", -5, -5)
    closeButton:SetScript("OnClick", function() dialog:Hide() end)
    UI.SkinCloseButton(closeButton)

    local sourcePanel = CreateFrame("Frame", nil, dialog, "BackdropTemplate")
    sourcePanel:SetPoint("TOPLEFT", dialog, "TOPLEFT", 14, -44)
    sourcePanel:SetPoint("TOPRIGHT", dialog, "TOPRIGHT", -14, -44)
    sourcePanel:SetHeight(62)
    UI.SkinFrame(sourcePanel, { light = true })

    dialog.sourceIcon = sourcePanel:CreateTexture(nil, "ARTWORK")
    dialog.sourceIcon:SetSize(42, 42)
    dialog.sourceIcon:SetPoint("LEFT", sourcePanel, "LEFT", 9, 0)
    dialog.sourceIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    dialog.sourceLabel = UI.CreateLabel(sourcePanel, TRANSLATE("Macro"), 10)
    dialog.sourceLabel:SetPoint("TOPLEFT", dialog.sourceIcon, "TOPRIGHT", 10, -2)
    dialog.sourceLabel:SetTextColor(0.62, 0.68, 0.78)

    dialog.sourceName = UI.CreateLabel(sourcePanel, "", 12)
    dialog.sourceName:SetPoint("TOPLEFT", dialog.sourceLabel, "BOTTOMLEFT", 0, -3)
    dialog.sourceName:SetPoint("RIGHT", sourcePanel, "RIGHT", -12, 0)
    dialog.sourceName:SetTextColor(1, 1, 1)

    dialog.sourceMacro = UI.CreateLabel(sourcePanel, "", 10)
    dialog.sourceMacro:SetPoint("BOTTOMLEFT", dialog.sourceIcon, "BOTTOMRIGHT", 10, 2)
    dialog.sourceMacro:SetPoint("RIGHT", sourcePanel, "RIGHT", -12, 0)
    dialog.sourceMacro:SetTextColor(0.7, 0.7, 0.7)

    local instruction = UI.CreateLabel(
        dialog,
        TRANSLATE("Assign replacements, add potions with +, or remove lines with X. Empty targets remain unchanged."),
        11
    )
    instruction:SetPoint("TOPLEFT", sourcePanel, "BOTTOMLEFT", 0, -12)
    instruction:SetPoint("RIGHT", dialog, "RIGHT", -14, 0)
    instruction:SetTextColor(0.8, 0.8, 0.8)

    local sourceColumnLabel = UI.CreateLabel(dialog, TRANSLATE("Detected potions"), 10)
    sourceColumnLabel:SetPoint("TOPLEFT", instruction, "BOTTOMLEFT", 8, -8)
    sourceColumnLabel:SetTextColor(0.62, 0.68, 0.78)

    local targetColumnLabel = UI.CreateLabel(dialog, TRANSLATE("Replacement"), 10)
    targetColumnLabel:SetPoint("TOPLEFT", instruction, "BOTTOMLEFT", 306, -8)
    targetColumnLabel:SetTextColor(0.62, 0.68, 0.78)

    local listScroll = CreateFrame("ScrollFrame", nil, dialog)
    listScroll:SetPoint("TOPLEFT", sourceColumnLabel, "BOTTOMLEFT", -8, -5)
    listScroll:SetPoint("BOTTOMRIGHT", dialog, "BOTTOMRIGHT", -34, 64)
    listScroll:EnableMouseWheel(true)
    if listScroll.SetClipsChildren then
        listScroll:SetClipsChildren(true)
    end

    local listChild = CreateFrame("Frame", nil, listScroll)
    listChild:SetSize(1, 1)
    listScroll:SetScrollChild(listChild)

    local scrollbar = CreateFrame("Slider", nil, dialog, "BackdropTemplate")
    scrollbar:SetPoint("TOPRIGHT", listScroll, "TOPRIGHT", 20, 0)
    scrollbar:SetPoint("BOTTOMRIGHT", listScroll, "BOTTOMRIGHT", 20, 0)
    scrollbar:SetWidth(10)
    scrollbar:SetOrientation("VERTICAL")
    scrollbar:SetValueStep(78)
    UI.SkinFrame(scrollbar, { light = true })
    scrollbar:SetThumbTexture("Interface\\Buttons\\WHITE8X8")
    local thumb = scrollbar:GetThumbTexture()
    if thumb then
        thumb:SetSize(10, 30)
        UI.SetAddonColor(thumb, "SetVertexColor")
    end
    scrollbar:SetScript("OnValueChanged", function(_, value)
        listScroll:SetVerticalScroll(value or 0)
    end)
    listScroll:SetScript("OnMouseWheel", function(_, delta)
        local minValue, maxValue = scrollbar:GetMinMaxValues()
        scrollbar:SetValue(math.max(minValue, math.min(
            maxValue,
            (scrollbar:GetValue() or 0) - (delta * 78)
        )))
    end)

    dialog.statusLabel = UI.CreateLabel(dialog, "", 10)
    dialog.statusLabel:SetPoint("BOTTOMLEFT", dialog, "BOTTOMLEFT", 14, 18)
    dialog.statusLabel:SetPoint("RIGHT", dialog, "RIGHT", -226, 0)
    dialog.statusLabel:SetJustifyH("LEFT")
    dialog.statusLabel:SetTextColor(1, 0.35, 0.28)

    local cancelButton = CreateFrame("Button", nil, dialog, "UIPanelButtonTemplate")
    cancelButton:SetSize(94, 26)
    cancelButton:SetPoint("BOTTOMRIGHT", dialog, "BOTTOMRIGHT", -14, 14)
    cancelButton:SetText(TRANSLATE("Cancel"))
    cancelButton:SetScript("OnClick", function() dialog:Hide() end)
    UI.SkinButton(cancelButton)

    dialog.replaceButton = CreateFrame("Button", nil, dialog, "UIPanelButtonTemplate")
    dialog.replaceButton:SetSize(132, 26)
    dialog.replaceButton:SetPoint("RIGHT", cancelButton, "LEFT", -8, 0)
    dialog.replaceButton:SetText(TRANSLATE("Apply changes"))
    UI.SkinButton(dialog.replaceButton)

    dialog.rows = {}
    dialog.addButton = CreateFrame("Button", nil, listChild, "BackdropTemplate")
    dialog.addButton:SetSize(30, 24)
    UI.SkinFrame(dialog.addButton, { light = true })
    dialog.addButton.text = dialog.addButton:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
    dialog.addButton.text:SetAllPoints(dialog.addButton)
    dialog.addButton.text:SetText("+")
    dialog.addButton.text:SetJustifyH("CENTER")
    UI.ApplyFont(dialog.addButton.text, 16, "")

    dialog.dragState = {
        active = false,
        sourceKey = nil,
        hoverRow = nil,
        placeAfter = false,
        ghost = nil,
        controller = nil,
    }
    dialog.nextEntryOrderID = 0

    local function GetEntryOrderKey(entry)
        if not entry then
            return nil
        end
        local lineNumber = tonumber(entry.lineNumber)
        if not entry.isNew and lineNumber then
            return "line:" .. tostring(lineNumber)
        end
        return entry.orderKey
    end

    local function EnsureEntryOrderKey(entry)
        if entry and not GetEntryOrderKey(entry) then
            dialog.nextEntryOrderID = dialog.nextEntryOrderID + 1
            entry.orderKey = "new:" .. tostring(dialog.nextEntryOrderID)
            entry.creationOrder = dialog.nextEntryOrderID
        end
        return GetEntryOrderKey(entry)
    end

    local function IsEntryIncludedInLineOrder(entry)
        if not entry or entry.removed then
            return false
        end
        if entry.isNew then
            return tonumber(entry.targetItemID) ~= nil
        end
        return tonumber(entry.lineNumber) ~= nil
    end

    function dialog:GetPotionLineOrder()
        local order = {}
        local seenLineNumbers = {}
        for _, entry in ipairs(self.entries or {}) do
            if IsEntryIncludedInLineOrder(entry) then
                local lineNumber = not entry.isNew and tonumber(entry.lineNumber)
                if lineNumber then
                    if not seenLineNumbers[lineNumber] then
                        seenLineNumbers[lineNumber] = true
                        order[#order + 1] = { lineNumber = lineNumber }
                    end
                else
                    order[#order + 1] = { targetItemID = tonumber(entry.targetItemID) }
                end
            end
        end
        return order
    end

    function dialog:HasPotionLineOrderChanged()
        local currentKeys = {}
        local seenCurrentLines = {}
        local visibleExistingLines = {}
        local visibleNewEntries = {}

        for _, entry in ipairs(self.entries or {}) do
            if IsEntryIncludedInLineOrder(entry) then
                local lineNumber = not entry.isNew and tonumber(entry.lineNumber)
                if lineNumber then
                    visibleExistingLines[lineNumber] = true
                    if not seenCurrentLines[lineNumber] then
                        seenCurrentLines[lineNumber] = true
                        currentKeys[#currentKeys + 1] = "line:" .. tostring(lineNumber)
                    end
                else
                    EnsureEntryOrderKey(entry)
                    visibleNewEntries[#visibleNewEntries + 1] = entry
                    currentKeys[#currentKeys + 1] = entry.orderKey
                end
            end
        end

        local defaultKeys = {}
        for _, lineNumber in ipairs(self.originalPotionLineOrder or {}) do
            if visibleExistingLines[lineNumber] then
                defaultKeys[#defaultKeys + 1] = "line:" .. tostring(lineNumber)
            end
        end
        table.sort(visibleNewEntries, function(left, right)
            return (left.creationOrder or 0) < (right.creationOrder or 0)
        end)
        for _, entry in ipairs(visibleNewEntries) do
            defaultKeys[#defaultKeys + 1] = entry.orderKey
        end

        if #currentKeys ~= #defaultKeys then
            return true
        end
        for index, key in ipairs(currentKeys) do
            if key ~= defaultKeys[index] then
                return true
            end
        end
        return false
    end

    local function SetPotionRowDragVisuals()
        local state = dialog.dragState
        local hoverKey = state.hoverRow and GetEntryOrderKey(state.hoverRow.entry)
        local boundaryRow
        if hoverKey then
            for _, candidate in ipairs(dialog.rows) do
                if candidate:IsShown() and GetEntryOrderKey(candidate.entry) == hoverKey then
                    if not boundaryRow or state.placeAfter then
                        boundaryRow = candidate
                    end
                end
            end
        end

        for _, row in ipairs(dialog.rows) do
            local shown = row:IsShown()
            local isSource = shown
                and state.active
                and GetEntryOrderKey(row.entry) == state.sourceKey
            row.dragOverlay:SetShown(isSource == true)
            row.dropBefore:SetShown(
                state.active
                    and not state.placeAfter
                    and boundaryRow == row
            )
            row.dropAfter:SetShown(
                state.active
                    and state.placeAfter
                    and boundaryRow == row
            )
        end
    end

    local function ResetPotionRowDrag()
        local state = dialog.dragState
        state.active = false
        state.sourceKey = nil
        state.hoverRow = nil
        state.placeAfter = false
        if state.ghost then
            state.ghost:Hide()
            state.ghost:ClearAllPoints()
        end
        SetPotionRowDragVisuals()
    end

    local function MovePotionEntryGroup(sourceKey, targetKey, placeAfter)
        if not sourceKey or not targetKey or sourceKey == targetKey then
            return false
        end

        local movingEntries = {}
        for index = #(dialog.entries or {}), 1, -1 do
            local entry = dialog.entries[index]
            if GetEntryOrderKey(entry) == sourceKey then
                table.insert(movingEntries, 1, table.remove(dialog.entries, index))
            end
        end
        if #movingEntries == 0 then
            return false
        end

        local targetFirst
        local targetLast
        for index, entry in ipairs(dialog.entries) do
            if GetEntryOrderKey(entry) == targetKey then
                targetFirst = targetFirst or index
                targetLast = index
            end
        end
        if not targetFirst then
            for _, entry in ipairs(movingEntries) do
                dialog.entries[#dialog.entries + 1] = entry
            end
            return false
        end

        local insertIndex = placeAfter and (targetLast + 1) or targetFirst
        for offset, entry in ipairs(movingEntries) do
            table.insert(dialog.entries, insertIndex + offset - 1, entry)
        end
        return true
    end

    local function EnsurePotionRowDragController()
        local state = dialog.dragState
        if state.controller then
            return
        end

        local ghost = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
        ghost:SetSize(246, 48)
        ghost:SetFrameStrata("TOOLTIP")
        ghost:SetFrameLevel(9999)
        UI.SkinFrame(ghost, { light = true })
        ghost:SetBackdropBorderColor(UI.GetClassColor())
        ghost.icon = ghost:CreateTexture(nil, "ARTWORK")
        ghost.icon:SetSize(36, 36)
        ghost.icon:SetPoint("LEFT", ghost, "LEFT", 6, 0)
        ghost.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        ghost.name = UI.CreateLabel(ghost, "", 10)
        ghost.name:SetPoint("LEFT", ghost.icon, "RIGHT", 8, 0)
        ghost.name:SetPoint("RIGHT", ghost, "RIGHT", -8, 0)
        ghost.name:SetJustifyH("LEFT")
        ghost:Hide()
        state.ghost = ghost

        local controller = CreateFrame("Frame", nil, UIParent)
        controller:SetScript("OnUpdate", function(_, elapsed)
            if not state.active then
                return
            end
            if addon:IsInCombat() or not dialog:IsShown() then
                ResetPotionRowDrag()
                return
            end

            local cursorX, cursorY = GetCursorPosition()
            local parentScale = UIParent:GetEffectiveScale() or 1
            ghost:ClearAllPoints()
            ghost:SetPoint(
                "TOPLEFT",
                UIParent,
                "BOTTOMLEFT",
                (cursorX / parentScale) + 12,
                (cursorY / parentScale) - 12
            )

            local scrollScale = listScroll:GetEffectiveScale() or 1
            local scaledCursorX = cursorX / scrollScale
            local scaledCursorY = cursorY / scrollScale
            local scrollLeft = listScroll:GetLeft()
            local scrollRight = listScroll:GetRight()
            local scrollTop = listScroll:GetTop()
            local scrollBottom = listScroll:GetBottom()
            if scrollLeft and scrollRight and scrollTop and scrollBottom
                and scaledCursorX >= scrollLeft and scaledCursorX <= scrollRight
            then
                local scrollValue = scrollbar:GetValue() or 0
                if scaledCursorY > scrollTop - 24 and scaledCursorY <= scrollTop then
                    scrollbar:SetValue(scrollValue - (240 * (elapsed or 0)))
                elseif scaledCursorY < scrollBottom + 24 and scaledCursorY >= scrollBottom then
                    scrollbar:SetValue(scrollValue + (240 * (elapsed or 0)))
                end
            end

            state.hoverRow = nil
            if scrollLeft and scrollRight and scrollTop and scrollBottom
                and scaledCursorX >= scrollLeft and scaledCursorX <= scrollRight
                and scaledCursorY >= scrollBottom and scaledCursorY <= scrollTop
            then
                for _, row in ipairs(dialog.rows) do
                    if row:IsShown() then
                        local rowScale = row:GetEffectiveScale() or 1
                        local rowCursorX = cursorX / rowScale
                        local rowCursorY = cursorY / rowScale
                        local left, right = row:GetLeft(), row:GetRight()
                        local top, bottom = row:GetTop(), row:GetBottom()
                        if left and right and top and bottom
                            and rowCursorX >= left and rowCursorX <= right
                            and rowCursorY >= bottom and rowCursorY <= top
                        then
                            local targetKey = GetEntryOrderKey(row.entry)
                            if targetKey ~= state.sourceKey then
                                state.hoverRow = row
                                state.placeAfter = rowCursorY < ((top + bottom) / 2)
                            end
                            break
                        end
                    end
                end
            end
            SetPotionRowDragVisuals()

            if not IsMouseButtonDown("LeftButton") then
                local hoverRow = state.hoverRow
                local targetKey = hoverRow and GetEntryOrderKey(hoverRow.entry)
                local sourceKey = state.sourceKey
                local placeAfter = state.placeAfter
                ResetPotionRowDrag()
                if MovePotionEntryGroup(sourceKey, targetKey, placeAfter) then
                    dialog.statusLabel:SetText(TRANSLATE("Pending changes have not been applied yet."))
                    dialog:Refresh()
                end
            end
        end)
        state.controller = controller
    end

    local function StartPotionRowDrag(row)
        if not row or not row.entry or dialog.dragState.active then
            return
        end
        if addon:IsInCombat() then
            dialog.statusLabel:SetText(TRANSLATE("Macros cannot be changed in combat."))
            dialog:RefreshMappings()
            return
        end

        EnsureEntryOrderKey(row.entry)
        EnsurePotionRowDragController()

        local state = dialog.dragState
        state.active = true
        state.sourceKey = GetEntryOrderKey(row.entry)
        state.hoverRow = nil
        state.placeAfter = false
        state.ghost.icon:SetTexture(row.sourceIcon:GetTexture() or DEFAULT_MACRO_ICON)
        state.ghost.name:SetText(row.sourceName:GetText() or "")
        state.ghost:Show()
        SetPotionRowDragVisuals()
    end

    local function EnsureRow(index)
        local row = dialog.rows[index]
        if row then
            return row
        end

        row = CreateFrame("Frame", nil, listChild, "BackdropTemplate")
        row:SetHeight(72)
        UI.SkinFrame(row, { light = true })

        row.dragOverlay = row:CreateTexture(nil, "BACKGROUND", nil, 2)
        row.dragOverlay:SetAllPoints(row)
        row.dragOverlay:SetColorTexture(0.25, 0.55, 1, 0.18)
        row.dragOverlay:Hide()

        row.dropBefore = row:CreateTexture(nil, "OVERLAY", nil, 7)
        row.dropBefore:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 1)
        row.dropBefore:SetPoint("TOPRIGHT", row, "TOPRIGHT", 0, 1)
        row.dropBefore:SetHeight(2)
        UI.SetAddonColor(row.dropBefore, "SetColorTexture")
        row.dropBefore:Hide()

        row.dropAfter = row:CreateTexture(nil, "OVERLAY", nil, 7)
        row.dropAfter:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, -1)
        row.dropAfter:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, -1)
        row.dropAfter:SetHeight(2)
        UI.SetAddonColor(row.dropAfter, "SetColorTexture")
        row.dropAfter:Hide()

        row.dragHandle = CreateFrame("Button", nil, row)
        row.dragHandle:SetPoint("TOPLEFT", row, "TOPLEFT", 2, -2)
        row.dragHandle:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 2, 2)
        row.dragHandle:SetWidth(22)
        for gripIndex = 1, 3 do
            local grip = row.dragHandle:CreateTexture(nil, "ARTWORK")
            grip:SetSize(10, 1)
            grip:SetPoint("CENTER", row.dragHandle, "CENTER", 0, (gripIndex - 2) * 4)
            grip:SetColorTexture(0.62, 0.68, 0.78, 0.9)
        end
        row.dragHandle:SetScript("OnMouseDown", function(_, button)
            if button == "LeftButton" then
                StartPotionRowDrag(row)
            end
        end)
        row.dragHandle:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            GameTooltip:AddLine(TRANSLATE("Drag to reorder potion lines"))
            GameTooltip:Show()
        end)
        row.dragHandle:SetScript("OnLeave", function()
            GameTooltip:Hide()
        end)

        row.sourceIcon = row:CreateTexture(nil, "ARTWORK")
        row.sourceIcon:SetSize(42, 42)
        row.sourceIcon:SetPoint("LEFT", row, "LEFT", 28, 0)
        row.sourceIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        row.sourceQuality = CreatePotionQualityBadge(row, row.sourceIcon)

        row.sourceName = UI.CreateLabel(row, "", 10)
        row.sourceName:SetPoint("TOPLEFT", row.sourceIcon, "TOPRIGHT", 8, -1)
        row.sourceName:SetWidth(184)
        row.sourceName:SetHeight(28)
        row.sourceName:SetJustifyV("TOP")
        row.sourceName:SetWordWrap(true)
        row.sourceName:SetTextColor(1, 1, 1)

        row.sourceDetails = UI.CreateLabel(row, "", 9)
        row.sourceDetails:SetPoint("BOTTOMLEFT", row.sourceIcon, "BOTTOMRIGHT", 8, 1)
        row.sourceDetails:SetWidth(184)
        row.sourceDetails:SetTextColor(0.68, 0.68, 0.68)

        row.arrow = row:CreateTexture(nil, "ARTWORK")
        row.arrow:SetTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Up")
        row.arrow:SetSize(22, 22)
        row.arrow:SetPoint("CENTER", row, "CENTER", -8, 0)

        row.targetButton = CreateFrame("Button", nil, row, "BackdropTemplate")
        row.targetButton:SetSize(222, 56)
        row.targetButton:SetPoint("RIGHT", row, "RIGHT", -31, 0)
        UI.SkinFrame(row.targetButton, { light = true })

        row.targetIcon = row.targetButton:CreateTexture(nil, "ARTWORK")
        row.targetIcon:SetSize(40, 40)
        row.targetIcon:SetPoint("LEFT", row.targetButton, "LEFT", 7, 0)
        row.targetIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        row.targetQuality = CreatePotionQualityBadge(row.targetButton, row.targetIcon)
        row.targetQuality:SetSize(20, 17)
        row.targetQuality:ClearAllPoints()
        row.targetQuality:SetPoint("TOPLEFT", row.targetIcon, "TOPLEFT", -4, 4)
        row.targetQuality2 = CreatePotionQualityBadge(row.targetButton, row.targetIcon)
        row.targetQuality2:SetSize(20, 17)
        row.targetQuality2:ClearAllPoints()
        row.targetQuality2:SetPoint("TOPLEFT", row.targetIcon, "TOPLEFT", 6, 4)

        row.targetName = UI.CreateLabel(row.targetButton, "", 10)
        row.targetName:SetPoint("TOPLEFT", row.targetIcon, "TOPRIGHT", 8, -2)
        row.targetName:SetPoint("RIGHT", row.targetButton, "RIGHT", -24, 0)
        row.targetName:SetHeight(20)
        row.targetName:SetJustifyV("TOP")
        row.targetName:SetWordWrap(true)
        row.targetName:SetTextColor(1, 1, 1)

        row.targetDetails = UI.CreateLabel(row.targetButton, "", 9)
        row.targetDetails:SetPoint("TOPLEFT", row.targetName, "BOTTOMLEFT", 0, 0)
        row.targetDetails:SetPoint("RIGHT", row.targetButton, "RIGHT", -24, 0)
        row.targetDetails:SetHeight(20)
        row.targetDetails:SetJustifyV("TOP")
        row.targetDetails:SetTextColor(0.68, 0.68, 0.68)

        row.clearButton = CreateFrame("Button", nil, row.targetButton)
        row.clearButton:SetSize(18, 18)
        row.clearButton:SetPoint("TOPRIGHT", row.targetButton, "TOPRIGHT", -2, -2)
        row.clearButton.icon = row.clearButton:CreateTexture(nil, "ARTWORK")
        row.clearButton.icon:SetPoint("TOPLEFT", row.clearButton, "TOPLEFT", 4, -4)
        row.clearButton.icon:SetPoint("BOTTOMRIGHT", row.clearButton, "BOTTOMRIGHT", -4, 4)
        row.clearButton.icon:SetTexture(REFRESH_ICON)
        row.clearButton:SetScript("OnClick", function()
            if row.entry then
                row.entry.targetItemID = nil
            end
            dialog.statusLabel:SetText("")
            dialog:RefreshMappings()
        end)
        row.clearButton:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            GameTooltip:AddLine(TRANSLATE("Clear replacement"))
            GameTooltip:Show()
        end)
        row.clearButton:SetScript("OnLeave", function()
            GameTooltip:Hide()
        end)

        row.removeButton = CreateFrame("Button", nil, row)
        row.removeButton:SetSize(22, 22)
        row.removeButton:SetPoint("TOPRIGHT", row, "TOPRIGHT", -3, -3)
        row.removeButton.text = row.removeButton:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
        row.removeButton.text:SetAllPoints(row.removeButton)
        row.removeButton.text:SetText("×")
        row.removeButton.text:SetJustifyH("CENTER")
        UI.ApplyFont(row.removeButton.text, 13, "")
        row.removeButton:SetScript("OnClick", function()
            local entry = row.entry
            if not entry then
                return
            end
            if entry.lineNumber then
                for _, candidate in ipairs(dialog.entries or {}) do
                    if candidate.lineNumber == entry.lineNumber then
                        candidate.removed = true
                    end
                end
            else
                entry.removed = true
            end
            dialog.statusLabel:SetText(TRANSLATE("Pending changes have not been applied yet."))
            dialog:Refresh()
        end)
        row.removeButton:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            GameTooltip:AddLine(TRANSLATE("Remove macro line"))
            GameTooltip:Show()
        end)
        row.removeButton:SetScript("OnLeave", function()
            GameTooltip:Hide()
        end)

        row.targetButton:SetScript("OnClick", function()
            dialog:OpenTargetPicker(row)
        end)
        row.targetButton:SetScript("OnEnter", function(self)
            local r, g, b = UI.GetClassColor()
            self:SetBackdropBorderColor(r, g, b, 0.8)
        end)
        row.targetButton:SetScript("OnLeave", function(self)
            self:SetBackdropBorderColor(0.28, 0.28, 0.32, 1)
        end)

        dialog.rows[index] = row
        return row
    end

    local picker = CreateFrame("Frame", nil, dialog, "BackdropTemplate")
    picker:SetSize(552, 374)
    picker:SetPoint("CENTER", dialog, "CENTER", 0, -6)
    picker:SetFrameLevel(dialog:GetFrameLevel() + 20)
    picker:EnableMouse(true)
    UI.SkinFrame(picker)
    picker:Hide()
    picker:SetScript("OnHide", function(self)
        local entry = self.mappingRow and self.mappingRow.entry
        if entry and entry.isNew and entry.pendingSelection and not entry.targetItemID then
            entry.removed = true
            if dialog.Refresh then
                dialog:Refresh()
            end
        end
        self.mappingRow = nil
    end)

    picker.title = UI.CreateLabel(picker, TRANSLATE("Choose replacement potion"), 13)
    picker.title:SetPoint("TOPLEFT", picker, "TOPLEFT", 12, -12)
    picker.title:SetTextColor(1, 1, 1)

    picker.source = UI.CreateLabel(picker, "", 10)
    picker.source:SetPoint("TOPLEFT", picker.title, "BOTTOMLEFT", 0, -5)
    picker.source:SetPoint("RIGHT", picker, "RIGHT", -38, 0)
    picker.source:SetTextColor(0.72, 0.72, 0.72)

    picker.explanation = UI.CreateLabel(
        picker,
        TRANSLATE("Potions already present or selected are hidden."),
        9
    )
    picker.explanation:SetPoint("TOPLEFT", picker.source, "BOTTOMLEFT", 0, -3)
    picker.explanation:SetPoint("RIGHT", picker, "RIGHT", -38, 0)
    picker.explanation:SetTextColor(0.55, 0.6, 0.68)

    local pickerClose = CreateFrame("Button", nil, picker, "UIPanelCloseButton")
    pickerClose:SetPoint("TOPRIGHT", picker, "TOPRIGHT", -4, -4)
    pickerClose:SetScript("OnClick", function() picker:Hide() end)
    UI.SkinCloseButton(pickerClose)

    local pickerScroll = CreateFrame("ScrollFrame", nil, picker)
    pickerScroll:SetPoint("TOPLEFT", picker, "TOPLEFT", 12, -74)
    pickerScroll:SetPoint("BOTTOMRIGHT", picker, "BOTTOMRIGHT", -30, 12)
    pickerScroll:EnableMouseWheel(true)
    if pickerScroll.SetClipsChildren then
        pickerScroll:SetClipsChildren(true)
    end

    local pickerChild = CreateFrame("Frame", nil, pickerScroll)
    pickerChild:SetSize(1, 1)
    pickerScroll:SetScrollChild(pickerChild)

    picker.emptyText = UI.CreateLabel(picker, TRANSLATE("No additional potions available."), 11)
    picker.emptyText:SetPoint("CENTER", pickerScroll, "CENTER")
    picker.emptyText:SetTextColor(0.68, 0.68, 0.68)
    picker.emptyText:Hide()

    local pickerScrollbar = CreateFrame("Slider", nil, picker, "BackdropTemplate")
    pickerScrollbar:SetPoint("TOPRIGHT", pickerScroll, "TOPRIGHT", 18, 0)
    pickerScrollbar:SetPoint("BOTTOMRIGHT", pickerScroll, "BOTTOMRIGHT", 18, 0)
    pickerScrollbar:SetWidth(10)
    pickerScrollbar:SetOrientation("VERTICAL")
    pickerScrollbar:SetValueStep(66)
    UI.SkinFrame(pickerScrollbar, { light = true })
    pickerScrollbar:SetThumbTexture("Interface\\Buttons\\WHITE8X8")
    local pickerThumb = pickerScrollbar:GetThumbTexture()
    if pickerThumb then
        pickerThumb:SetSize(10, 30)
        UI.SetAddonColor(pickerThumb, "SetVertexColor")
    end
    pickerScrollbar:SetScript("OnValueChanged", function(_, value)
        pickerScroll:SetVerticalScroll(value or 0)
    end)
    pickerScroll:SetScript("OnMouseWheel", function(_, delta)
        local minValue, maxValue = pickerScrollbar:GetMinMaxValues()
        pickerScrollbar:SetValue(math.max(minValue, math.min(
            maxValue,
            (pickerScrollbar:GetValue() or 0) - (delta * 66)
        )))
    end)

    picker.rows = {}
    local function EnsurePickerRow(index)
        local option = picker.rows[index]
        if option then
            return option
        end

        option = CreateFrame("Button", nil, pickerChild, "BackdropTemplate")
        option:SetSize(246, 58)
        UI.SkinFrame(option, { light = true })

        option.icon = option:CreateTexture(nil, "ARTWORK")
        option.icon:SetSize(40, 40)
        option.icon:SetPoint("LEFT", option, "LEFT", 7, 0)
        option.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        option.quality = CreatePotionQualityBadge(option, option.icon)
        option.quality:SetSize(20, 17)
        option.quality:ClearAllPoints()
        option.quality:SetPoint("TOPLEFT", option.icon, "TOPLEFT", -4, 4)
        option.quality2 = CreatePotionQualityBadge(option, option.icon)
        option.quality2:SetSize(20, 17)
        option.quality2:ClearAllPoints()
        option.quality2:SetPoint("TOPLEFT", option.icon, "TOPLEFT", 6, 4)

        option.name = UI.CreateLabel(option, "", 10)
        option.name:SetPoint("TOPLEFT", option.icon, "TOPRIGHT", 8, -1)
        option.name:SetPoint("RIGHT", option, "RIGHT", -6, 0)
        option.name:SetHeight(18)
        option.name:SetJustifyV("TOP")
        option.name:SetWordWrap(true)
        option.name:SetTextColor(1, 1, 1)

        option.details = UI.CreateLabel(option, "", 9)
        option.details:SetPoint("TOPLEFT", option.name, "BOTTOMLEFT", 0, 0)
        option.details:SetPoint("RIGHT", option, "RIGHT", -6, 0)
        option.details:SetHeight(20)
        option.details:SetJustifyV("TOP")

        option:SetScript("OnClick", function(self)
            if picker.mappingRow and self.item then
                local entry = picker.mappingRow.entry
                if entry then
                    entry.targetItemID = self.item.id
                    entry.pendingSelection = nil
                end
                dialog.statusLabel:SetText("")
                dialog:RefreshMappings()
            end
            picker:Hide()
        end)
        option:SetScript("OnEnter", function(self)
            local r, g, b = UI.GetClassColor()
            self:SetBackdropBorderColor(r, g, b, 0.8)
            if self.item then
                GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                GameTooltip:AddLine(self.item.name or string.format("Item %d", self.item.id))
                for _, variant in ipairs(self.item.variants or { self.item }) do
                    GameTooltip:AddLine(GetPotionDetailText(variant), 0.72, 0.72, 0.72)
                end
                GameTooltip:AddLine(string.format(TRANSLATE("Owned: %d"), self.item.count or 0), 0.72, 0.72, 0.72)
                GameTooltip:Show()
            end
        end)
        option:SetScript("OnLeave", function(self)
            self:SetBackdropBorderColor(0.28, 0.28, 0.32, 1)
            GameTooltip:Hide()
        end)

        picker.rows[index] = option
        return option
    end

    function dialog:RefreshMappings()
        local hasChanges = false
        for _, row in ipairs(self.rows) do
            if row:IsShown() then
                local entry = row.entry or {}
                local target = self.targetCatalogByID
                    and self.targetCatalogByID[entry.targetItemID]
                if target then
                    row.targetIcon:SetTexture(target.icon or DEFAULT_MACRO_ICON)
                    row.targetIcon:SetDesaturated(false)
                    row.targetIcon:SetAlpha(1)
                    SetPotionQualityBadges(
                        row.targetQuality,
                        row.targetQuality2,
                        target
                    )
                    row.targetName:SetText(target.name or string.format("Item %d", target.id))
                    row.targetDetails:SetText(GetPotionDetailText(target))
                    row.clearButton:Show()
                    hasChanges = true
                else
                    row.targetIcon:SetTexture(DEFAULT_MACRO_ICON)
                    row.targetIcon:SetDesaturated(true)
                    row.targetIcon:SetAlpha(0.55)
                    SetPotionQualityBadges(
                        row.targetQuality,
                        row.targetQuality2,
                        nil
                    )
                    row.targetName:SetText(TRANSLATE("Choose replacement"))
                    row.targetDetails:SetText(TRANSLATE("This potion will remain unchanged."))
                    row.clearButton:Hide()
                end
            end
        end
        for _, entry in ipairs(self.entries or {}) do
            if entry.removed and not entry.isNew then
                hasChanges = true
            end
        end
        if self:HasPotionLineOrderChanged() then
            hasChanges = true
        end

        if hasChanges and not addon:IsInCombat() then
            self.replaceButton:Enable()
            self.replaceButton:SetAlpha(1)
        else
            self.replaceButton:Disable()
            self.replaceButton:SetAlpha(0.45)
        end
    end

    function dialog:OpenTargetPicker(mappingRow)
        picker.mappingRow = mappingRow
        local entry = mappingRow.entry or {}
        local source = entry.sourcePotion or {}
        picker.source:SetText(string.format(
            TRANSLATE("Replacement for: %s"),
            entry.isNew and TRANSLATE("New potion")
                or source.name
                or source.sourceText
                or TRANSLATE("Unknown potion")
        ))

        for _, option in ipairs(picker.rows) do
            option:Hide()
        end

        local columns = 2
        local rowHeight = 58
        local rowGap = 8
        local columnGap = 8
        local unavailableItemIDs = {}
        for _, candidate in ipairs(self.entries or {}) do
            if not candidate.removed then
                local source = candidate.sourcePotion or {}
                local sourceItemID = tonumber(source.id)
                if sourceItemID then
                    unavailableItemIDs[sourceItemID] = true
                end
                for _, candidateItemID in ipairs(source.candidateIDs or {}) do
                    candidateItemID = tonumber(candidateItemID)
                    if candidateItemID then
                        unavailableItemIDs[candidateItemID] = true
                    end
                end

                local targetItemID = tonumber(candidate.targetItemID)
                if targetItemID then
                    unavailableItemIDs[targetItemID] = true
                end
            end
        end

        local optionCount = 0
        for _, item in ipairs(self.targetCatalog or {}) do
            local unavailable = false
            for _, variantID in ipairs(item.variantIDs or { item.id }) do
                if unavailableItemIDs[variantID] then
                    unavailable = true
                    break
                end
            end
            if not unavailable then
                optionCount = optionCount + 1
                local option = EnsurePickerRow(optionCount)
                option.item = item
                option.icon:SetTexture(item.icon or DEFAULT_MACRO_ICON)
                SetPotionQualityBadges(option.quality, option.quality2, item)
                option.name:SetText(item.name or string.format("Item %d", item.id))
                option.details:SetText(GetPotionDetailText(item))
                option.details:SetTextColor(0.62, 0.72, 0.82)

                local column = (optionCount - 1) % columns
                local itemRow = math.floor((optionCount - 1) / columns)
                option:ClearAllPoints()
                option:SetPoint(
                    "TOPLEFT",
                    pickerChild,
                    "TOPLEFT",
                    column * (246 + columnGap),
                    -(itemRow * (rowHeight + rowGap))
                )
                option:Show()
            end
        end

        picker.emptyText:SetShown(optionCount == 0)
        local rowCount = math.max(1, math.ceil(optionCount / columns))
        local contentHeight = math.max((rowCount * (rowHeight + rowGap)) - rowGap, 1)
        pickerChild:SetSize(math.max(pickerScroll:GetWidth() or 1, 1), contentHeight)
        local maxScroll = math.max(contentHeight - (pickerScroll:GetHeight() or 0), 0)
        pickerScrollbar:SetMinMaxValues(0, maxScroll)
        pickerScrollbar:SetValue(0)
        pickerScrollbar:SetShown(maxScroll > 0)
        picker:Show()
        picker:Raise()
    end

    function dialog:Refresh()
        if self.dragState.active then
            ResetPotionRowDrag()
        end

        local macro = self.macro or {}
        local potionOccurrences = macro.combatPotionOccurrences
            or macro.combatPotions
            or (macro.combatPotion and { macro.combatPotion })
            or {}
        self.sourceIcon:SetTexture(GetMacroIcon(macro.icon))
        self.sourceName:SetText(macro.name or TRANSLATE("Unnamed macro"))
        self.sourceMacro:SetText(string.format(
            TRANSLATE("%d combat potions detected"),
            #potionOccurrences
        ))

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

        self.catalog = addon.GetCombatPotionCatalog and addon:GetCombatPotionCatalog() or {}
        self.catalogByID = {}
        for _, item in ipairs(self.catalog) do
            self.catalogByID[item.id] = item
        end
        self.targetCatalog = addon.GetCombatPotionTargetCatalog
            and addon:GetCombatPotionTargetCatalog()
            or self.catalog
        self.targetCatalogByID = {}
        for _, item in ipairs(self.targetCatalog) do
            self.targetCatalogByID[item.id] = item
            for _, variantID in ipairs(item.variantIDs or {}) do
                self.targetCatalogByID[variantID] = item
            end
        end

        local rowHeight = 72
        local rowGap = 8
        local visibleCount = 0
        for _, entry in ipairs(self.entries or {}) do
            if not entry.removed then
                visibleCount = visibleCount + 1
                local row = EnsureRow(visibleCount)
                local source = entry.sourcePotion or {}
                local sourceItem = self.catalogByID[tonumber(source.id)]
                row.entry = entry
                row.sourceIcon:SetTexture(
                    entry.isNew and DEFAULT_MACRO_ICON
                        or source.icon
                        or DEFAULT_MACRO_ICON
                )
                SetPotionQualityBadge(
                    row.sourceQuality,
                    entry.isNew and nil or sourceItem
                )
                row.sourceName:SetText(
                    entry.isNew and TRANSLATE("New potion")
                        or source.name
                        or source.sourceText
                        or TRANSLATE("Unknown potion")
                )
                if entry.isNew then
                    row.sourceDetails:SetText(TRANSLATE("Will be added as /use potion name"))
                elseif tonumber(source.id) then
                    row.sourceDetails:SetText(GetPotionDetailText(
                        sourceItem,
                        source.id
                    ))
                else
                    row.sourceDetails:SetText(TRANSLATE("Potion name used in macro"))
                end

                row:ClearAllPoints()
                row:SetPoint(
                    "TOPLEFT",
                    listChild,
                    "TOPLEFT",
                    0,
                    -((visibleCount - 1) * (rowHeight + rowGap))
                )
                row:SetPoint("RIGHT", listChild, "RIGHT", 0, 0)
                row:Show()
            end
        end

        self.addButton:ClearAllPoints()
        self.addButton:SetPoint(
            "TOP",
            listChild,
            "TOP",
            0,
            -((visibleCount * (rowHeight + rowGap)) + 2)
        )
        self.addButton:Show()

        local contentHeight = math.max(
            (visibleCount * (rowHeight + rowGap)) + self.addButton:GetHeight() + 6,
            1
        )
        listChild:SetSize(math.max(listScroll:GetWidth() or 1, 1), contentHeight)
        local maxScroll = math.max(contentHeight - (listScroll:GetHeight() or 0), 0)
        scrollbar:SetMinMaxValues(0, maxScroll)
        scrollbar:SetValue(math.min(scrollbar:GetValue() or 0, maxScroll))
        scrollbar:SetShown(maxScroll > 0)

        if addon:IsInCombat() then
            self.statusLabel:SetText(TRANSLATE("Macros cannot be changed in combat."))
        elseif self.statusLabel:GetText() == TRANSLATE("Macros cannot be changed in combat.") then
            self.statusLabel:SetText("")
        end
        self:RefreshMappings()
        SetPotionRowDragVisuals()
    end

    function dialog:AddPotion()
        local entry = {
            isNew = true,
            pendingSelection = true,
        }
        EnsureEntryOrderKey(entry)
        self.entries[#self.entries + 1] = entry
        self.statusLabel:SetText(TRANSLATE("Pending changes have not been applied yet."))
        self:Refresh()
        for _, row in ipairs(self.rows) do
            if row:IsShown() and row.entry == entry then
                self:OpenTargetPicker(row)
                break
            end
        end
    end

    dialog.addButton:SetScript("OnClick", function()
        dialog:AddPotion()
    end)
    dialog.addButton:SetScript("OnEnter", function(self)
        local r, g, b = UI.GetClassColor()
        self:SetBackdropBorderColor(r, g, b, 0.9)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:AddLine(TRANSLATE("Add combat potion"))
        GameTooltip:Show()
    end)
    dialog.addButton:SetScript("OnLeave", function(self)
        self:SetBackdropBorderColor(0.28, 0.28, 0.32, 1)
        GameTooltip:Hide()
    end)

    function dialog:Open(macro)
        self.macro = macro
        self.entries = {}
        self.nextEntryOrderID = 0
        self.originalPotionLineOrder = {}
        local seenLineNumbers = {}
        local potionOccurrences = macro.combatPotionOccurrences
            or macro.combatPotions
            or (macro.combatPotion and { macro.combatPotion })
            or {}
        for _, source in ipairs(potionOccurrences) do
            local lineNumber = tonumber(source.lineNumber)
            self.entries[#self.entries + 1] = {
                sourcePotion = source,
                lineNumber = lineNumber,
            }
            if lineNumber and not seenLineNumbers[lineNumber] then
                seenLineNumbers[lineNumber] = true
                self.originalPotionLineOrder[#self.originalPotionLineOrder + 1] = lineNumber
            end
        end
        self.statusLabel:SetText("")
        picker:Hide()
        self:Refresh()
        self:Show()
        self:Raise()
    end

    dialog.replaceButton:SetScript("OnClick", function()
        if not dialog.macro then
            return
        end

        local changes = {
            replacements = {},
            removedLineNumbers = {},
            additions = {},
            lineOrder = dialog:GetPotionLineOrder(),
        }
        for _, entry in ipairs(dialog.entries or {}) do
            if entry.removed then
                if not entry.isNew and entry.lineNumber then
                    changes.removedLineNumbers[entry.lineNumber] = true
                end
            elseif entry.isNew then
                if entry.targetItemID then
                    changes.additions[#changes.additions + 1] = entry.targetItemID
                end
            elseif entry.targetItemID and entry.lineNumber then
                changes.replacements[#changes.replacements + 1] = {
                    lineNumber = entry.lineNumber,
                    sourcePotion = entry.sourcePotion,
                    targetItemID = entry.targetItemID,
                }
            end
        end

        local success, reason = addon:ApplyCombatPotionMacroChanges(dialog.macro, changes)
        if success then
            dialog:Hide()
            return
        end

        local messages = {
            combat = "Macros cannot be changed in combat.",
            macroChanged = "The macro changed. Refresh the list and try again.",
            noChange = "The selected potion is already used by this macro.",
            tooLong = "The macro is too long after replacement.",
            editFailed = "The macro could not be updated.",
            invalidTarget = "The selected potion is no longer available.",
            targetAlreadyPresent = "The selected potion is already present in the macro.",
            duplicateTarget = "Each replacement potion can only be selected once.",
            invalidOrder = "The potion line order is no longer valid.",
            noSelection = "No pending changes.",
        }
        dialog.statusLabel:SetText(TRANSLATE(messages[reason] or "The macro could not be updated."))
        dialog:RefreshMappings()
    end)

    dialog:SetScript("OnShow", function(self) self:Refresh() end)
    dialog:SetScript("OnHide", function()
        ResetPotionRowDrag()
        picker:Hide()
        GameTooltip:Hide()
    end)
    dialog:Hide()

    addon.combatPotionUpgradeDialog = dialog
    function addon:RefreshCombatPotionUpgradeDialog()
        if self.combatPotionUpgradeDialog and self.combatPotionUpgradeDialog:IsShown() then
            self.combatPotionUpgradeDialog:Refresh()
        end
    end

    return dialog
end

WipedQoL:RegisterTabBuilder("gameplay", function(addon, parent)
    local frame = CreateFrame("Frame", nil, parent)
    frame:HookScript("OnHide", HideMacroTooltip)
    local autoSignUpSettings = addon:GetAutoSignUpSettings()
    local autoSignUpCheckbox
    local autoSignUpModifierLabel
    local autoSignUpModifierDropdown

    local function RefreshAutoSignUpState()
        autoSignUpSettings = addon:GetAutoSignUpSettings()
        local enabled = autoSignUpSettings.enabled == true

        if autoSignUpCheckbox and autoSignUpCheckbox.Refresh then
            autoSignUpCheckbox.Refresh()
        end

        if autoSignUpModifierLabel then
            autoSignUpModifierLabel:SetTextColor(enabled and 0.78 or 0.38, enabled and 0.78 or 0.38, enabled and 0.78 or 0.38)
        end

        if autoSignUpModifierDropdown then
            autoSignUpModifierDropdown:SetAlpha(enabled and 1 or 0.45)
            if autoSignUpModifierDropdown._wqolClickOverlay then
                autoSignUpModifierDropdown._wqolClickOverlay:EnableMouse(enabled)
            end
        end
    end

    function addon:RefreshAutoSignUpGameplayUI()
        RefreshAutoSignUpState()
    end

    local autoSignUpSection = UI.CreateSection(
        frame,
        TRANSLATE("Auto sign up"),
        TRANSLATE("Automatically signs you up when you click a group finder listing and confirms your role selection. Also skips role selection when the group leader queues the group. Hold the selected key to pause.")
    )
    autoSignUpSection:SetPoint("TOPLEFT", frame, "TOPLEFT", 14, -14)
    autoSignUpSection:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -14, -14)
    autoSignUpSection:SetHeight(104)

    autoSignUpCheckbox = UI.CreateSectionToggle(autoSignUpSection, function()
        return autoSignUpSettings.enabled == true
    end, function(value)
        addon:SetAutoSignUpEnabled(value)
        RefreshAutoSignUpState()
    end)

    autoSignUpModifierLabel = UI.CreateLabel(autoSignUpSection, TRANSLATE("Temporarily disable"), 12)
    autoSignUpModifierLabel:SetPoint("TOPLEFT", autoSignUpSection.description, "BOTTOMLEFT", 0, -10)
    autoSignUpModifierLabel:SetTextColor(0.78, 0.78, 0.78)

    autoSignUpModifierDropdown = UI.CreateDropdown(autoSignUpSection, 80, MODIFIER_OPTIONS, function()
        return autoSignUpSettings.modifier or "SHIFT"
    end, function(value)
        addon:SetAutoSignUpModifier(value)
        autoSignUpSettings = addon:GetAutoSignUpSettings()
    end)
    autoSignUpModifierDropdown:SetPoint("LEFT", autoSignUpModifierLabel, "RIGHT", -12, 0)

    local healthSection = UI.CreateSection(
        frame,
        TRANSLATE("Healing macro"),
        TRANSLATE("A healing macro for [nocombat] Recuperate, [combat] Healthstone and healing potions. Drag it from here directly onto an action bar.")
    )
    healthSection:SetPoint("TOPLEFT", autoSignUpSection, "BOTTOMLEFT", 0, -12)
    healthSection:SetSize(280, PREVIEW_ONE_ROW_HEIGHT)

    UI.SetSectionDescriptionInsets(healthSection, 10, 62)

    local healthMacroButton = CreateHealthMacroButton(healthSection)
    healthMacroButton:SetPoint("RIGHT", healthSection, "RIGHT", -12, 14)
    local classHealingSpellButton = CreateFrame("Button", nil, healthSection, "UIPanelButtonTemplate")
    classHealingSpellButton:SetSize(260, 24)
    classHealingSpellButton:SetPoint("BOTTOMLEFT", healthSection, "BOTTOMLEFT", 10, 9)
    classHealingSpellButton:SetText(TRANSLATE("Add class healing spells"))
    classHealingSpellButton:SetScript("OnClick", function()
        local editor = EnsureHealthSpellEditor(addon)
        editor:SetShown(not editor:IsShown())
    end)
    UI.SkinButton(classHealingSpellButton)

    function addon:RefreshHealthMacroGameplayUI()
        if addon.healthSpellEditor and addon.healthSpellEditor:IsShown() then
            addon.healthSpellEditor:Refresh()
        end
    end

    local function RefreshHealthMacroIcon()
        healthMacroButton.icon:SetTexture(addon.GetHealthMacroIcon and addon:GetHealthMacroIcon() or DEFAULT_MACRO_ICON)
        healthMacroButton.countText:SetText(addon.GetHealthPotionCount and addon:GetHealthPotionCount() or 0)
    end
    RefreshHealthMacroIcon()
    frame:RegisterEvent("BAG_UPDATE_DELAYED")
    frame:SetScript("OnEvent", function(_, event)
        if event == "BAG_UPDATE_DELAYED" then
            RefreshHealthMacroIcon()
            if addon.RefreshCombatPotionUpgradeDialog then
                addon:RefreshCombatPotionUpgradeDialog()
            end
        end
    end)
    frame:SetScript("OnShow", function()
        RefreshHealthMacroIcon()
        if addon.RefreshCombatPotionUpgradeDialog then
            addon:RefreshCombatPotionUpgradeDialog()
        end
    end)
    frame:SetScript("OnHide", function()
        if addon.healthSpellEditor then
            addon.healthSpellEditor:Hide()
        end
        if addon.combatPotionUpgradeDialog then
            addon.combatPotionUpgradeDialog:Hide()
        end
    end)
    healthMacroButton:SetScript("OnClick", function()
        RefreshHealthMacroIcon()
        if addon.OpenHealthMacro then
            addon:OpenHealthMacro()
            return
        end

        if addon.EnsureHealthMacro then
            addon:EnsureHealthMacro()
        end
    end)
    healthMacroButton:SetScript("OnEnter", function(self)
        RefreshHealthMacroIcon()
        local r, g, b = UI.GetClassColor()
        for _, texture in pairs(self.border) do
            texture:SetColorTexture(r, g, b, 0.95)
        end
        self.AnimateIconZoom(0)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:AddLine(TRANSLATE("Healing macro"))
        GameTooltip:AddLine(TRANSLATE("Drag onto actionbar."), 0.78, 0.78, 0.78)
        GameTooltip:Show()
    end)
    healthMacroButton:SetScript("OnLeave", function(self)
        for _, texture in pairs(self.border) do
            texture:SetColorTexture(1, 1, 1, 0.18)
        end
        self.AnimateIconZoom(2)
        GameTooltip:Hide()
    end)
    healthMacroButton:SetScript("OnDragStart", function()
        if addon:IsInCombat() then
            return
        end

        RefreshHealthMacroIcon()
        local macroIndex = addon.EnsureHealthMacro and addon:EnsureHealthMacro()
        if macroIndex then
            PickupMacro(macroIndex)
        end
    end)

    local previewFrame = UI.CreateSection(
        frame,
        TRANSLATE("Combat potion macros"),
        TRANSLATE("Enables or disables combat potion lines in detected macros.\nDrag macros from here directly onto an action bar.")
    )
    previewFrame:SetPoint("TOPLEFT", healthSection, "TOPRIGHT", 14, 0)
    previewFrame:SetPoint("RIGHT", frame, "RIGHT", -14, 0)
    previewFrame:SetHeight(PREVIEW_ONE_ROW_HEIGHT)

    local refreshButton = CreateIconButton(previewFrame, REFRESH_ICON)
    refreshButton:SetPoint("TOPRIGHT", previewFrame, "TOPRIGHT", -8, -3)

    local mainToggle = UI.CreateToggleSwitch(previewFrame, 38, 18)
    mainToggle:SetPoint("RIGHT", refreshButton, "LEFT", -8, 0)
    mainToggle:SetScript("OnClick", function(self)
        local targetActive = not self:GetChecked()
        self:SetChecked(targetActive, true)

        if targetActive then
            if addon.UnpauseCombatPotionInMacros then
                addon:UnpauseCombatPotionInMacros()
            end
        elseif addon.PauseCombatPotionInMacros then
            addon:PauseCombatPotionInMacros()
        end
    end)

    local mainToggleLabel = UI.CreateLabel(previewFrame, TRANSLATE("Toggle all"), 11)
    mainToggleLabel:SetPoint("RIGHT", mainToggle, "LEFT", -8, 0)
    mainToggleLabel:SetTextColor(0.78, 0.78, 0.78)

    previewFrame.title:ClearAllPoints()
    previewFrame.title:SetPoint("TOPLEFT", previewFrame, "TOPLEFT", 10, -8)
    refreshButton:SetScript("OnClick", function()
        if addon.RefreshCombatPotionMacroPreview then
            addon:RefreshCombatPotionMacroPreview()
        end
    end)
    refreshButton:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:AddLine(TRANSLATE("Refresh"))
        GameTooltip:Show()
    end)
    refreshButton:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)

    local emptyText = previewFrame:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
    emptyText:SetPoint("TOPLEFT", previewFrame.description, "BOTTOMLEFT", 0, -10)
    emptyText:SetPoint("RIGHT", previewFrame, "RIGHT", -10, 0)
    emptyText:SetJustifyH("LEFT")
    emptyText:SetTextColor(0.78, 0.78, 0.78, 1)
    UI.ApplyFont(emptyText, 12, "")

    local cards = {}

    local function ResolvePreviewMacroIndex(macro)
        local index = macro and macro.index
        if addon.ResolveCombatPotionMacroIndex then
            index = addon:ResolveCombatPotionMacroIndex(macro)
        end
        return index
    end

    local function PickupPreviewMacro(macro)
        if addon:IsInCombat() then
            return
        end

        local index = ResolvePreviewMacroIndex(macro)
        if index then
            HideMacroTooltip()
            GameTooltip:Hide()
            PickupMacro(index)
        end
    end

    function addon:RefreshCombatPotionMacroPreviewUI()
        local items = addon.GetCombatPotionMacroPreviewItems and addon:GetCombatPotionMacroPreviewItems() or {}
        local rowCount = math.max(1, math.ceil(#items / CARD_COLUMNS))
        previewFrame:SetHeight(PREVIEW_ONE_ROW_HEIGHT + ((rowCount - 1) * (CARD_HEIGHT + CARD_ROW_GAP)))

        local allActive = false
        local mixedActive = false
        if addon.GetCombatPotionMacroActiveState then
            allActive, _, mixedActive = addon:GetCombatPotionMacroActiveState()
        else
            local hasActive = false
            local hasPaused = false
            for _, item in ipairs(items) do
                if item.paused then
                    hasPaused = true
                else
                    hasActive = true
                end
            end
            allActive = hasActive and not hasPaused
            mixedActive = hasActive and hasPaused
        end

        mainToggle:SetAlpha(#items > 0 and 1 or 0.45)
        mainToggle:EnableMouse(#items > 0)
        mainToggleLabel:SetAlpha(#items > 0 and 1 or 0.45)
        if mixedActive and mainToggle.SetMixed then
            mainToggle:SetMixed(true, true)
        else
            mainToggle:SetChecked(allActive, true)
        end

        emptyText:SetShown(#items == 0)
        if #items == 0 then
            emptyText:SetText(TRANSLATE("No matching macros found."))
        end

        for index, card in ipairs(cards) do
            card:SetShown(index <= #items)
        end

        for index, item in ipairs(items) do
            local card = cards[index]
            if not card then
                card = CreateMacroCard(previewFrame)
                cards[index] = card
            end

            local column = (index - 1) % CARD_COLUMNS
            local row = math.floor((index - 1) / CARD_COLUMNS)
            card:ClearAllPoints()
            card:SetPoint(
                "TOPLEFT",
                previewFrame.description,
                "BOTTOMLEFT",
                -2 + (column * (CARD_WIDTH + CARD_GAP)),
                -8 - (row * (CARD_HEIGHT + CARD_ROW_GAP))
            )

            card.icon:SetTexture(GetMacroIcon(item.icon))
            card.icon:SetDesaturated(item.paused == true)
            card.icon:SetAlpha(item.paused and 0.58 or 1)
            card.dim:SetVertexColor(0, 0, 0, item.paused and 0.38 or 0)
            card.name:SetText(item.name or TRANSLATE("Unnamed macro"))
            card.item = item
            card.upgradeButton:SetShown(item.combatPotion ~= nil)
            card.toggle:SetChecked(item.paused ~= true, true)
            card.toggle:SetScript("OnClick", function(self)
                local macro = self:GetParent().item
                if not macro or not macro.index then
                    return
                end

                local targetActive = not self:GetChecked()
                self:SetChecked(targetActive, true)
                if addon.SetCombatPotionMacroPaused then
                    addon:SetCombatPotionMacroPaused(macro.index, not targetActive)
                end
            end)

            local function EnterMacroCard(self)
                local macroCard = self.icon and self or self:GetParent()
                local r, g, b = UI.GetClassColor()
                macroCard:SetIconBorderColor(r, g, b, 0.95)
                macroCard.AnimateIconZoom(0)
                ShowMacroTooltip(self, macroCard.item)
            end
            local function LeaveMacroCard(self)
                local macroCard = self.icon and self or self:GetParent()
                macroCard:SetIconBorderColor(1, 1, 1, 0.18)
                macroCard.AnimateIconZoom(2)
                HideMacroTooltip()
            end

            card:SetScript("OnEnter", EnterMacroCard)
            card:SetScript("OnLeave", LeaveMacroCard)
            card.iconButton:SetScript("OnEnter", function(self)
                EnterMacroCard(self)
            end)
            card.iconButton:SetScript("OnLeave", function(self)
                LeaveMacroCard(self)
            end)
            card.iconButton:SetScript("OnClick", function(self)
                local macro = self:GetParent().item or {}
                local index = ResolvePreviewMacroIndex(macro)

                HideMacroTooltip()
                if addon.OpenCombatPotionMacro then
                    addon:OpenCombatPotionMacro(index)
                end
            end)
            card.upgradeButton:SetScript("OnClick", function(self)
                local macro = self:GetParent().item
                if not macro or not macro.combatPotion then
                    return
                end
                HideMacroTooltip()
                EnsureCombatPotionUpgradeDialog(addon):Open(macro)
            end)
            card.upgradeButton:SetScript("OnEnter", function(self)
                local r, g, b = UI.GetClassColor()
                HideMacroTooltip()
                SetButtonHoverBorder(self, r, g, b, 1)
                GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                GameTooltip:AddLine(TRANSLATE("Replace combat potion"))
                GameTooltip:AddLine(TRANSLATE("Click to configure macro."), 0.78, 0.78, 0.78)
                GameTooltip:Show()
            end)
            card.upgradeButton:SetScript("OnLeave", function(self)
                SetButtonHoverBorder(self, 0, 0, 0, 0)
                GameTooltip:Hide()
            end)
            card:SetScript("OnDragStart", function(self)
                PickupPreviewMacro(self.item)
            end)
            card.iconButton:SetScript("OnDragStart", function(self)
                PickupPreviewMacro(self:GetParent().item)
            end)
            card:Show()
        end
    end

    if addon.RefreshCombatPotionMacroPreview then
        addon:RefreshCombatPotionMacroPreview()
    else
        addon:RefreshCombatPotionMacroPreviewUI()
    end
    RefreshAutoSignUpState()

    return frame
end)
