local addonPrefix = "GroupSwapper"
local printPrefix = "[GroupSwapper]: "
local debugEnabled = false
local colorReset = "|r"
local colorTitle = "|cffffff00"
local colorHeader = "|cffaad372"
local colorCommand = "|cff66ccff"
local colorValue = "|cffffd100"
local configFrame
local configSwapperValue
local configStatusText
local configPlayerOneEditBox
local configPlayerTwoEditBox
local configSetNameEditBox
local configSelectedSetValue
local configTargetSwapTimeoutValue
local configSetButtons = {}
local configSetPairRows = {}
local buttonBarFrame
local buttonBarButtons = {}
local buttonBarStatusText
local RefreshButtonBar
local CreateButtonBar
local GetTargetSwapTimeout
local maxConfigSetRows = 5
local maxConfigSetButtons = 5
local maxActiveSwapGroup = 5
local setSwapMessagePrefix = "SET_SWAP:"
local pendingTargetSwapName
local pendingTargetSwapTime
local print = _G.print

if not print then
    print = function(...)
        if not DEFAULT_CHAT_FRAME or not DEFAULT_CHAT_FRAME.AddMessage then
            return
        end

        local parts = {}
        for index = 1, select("#", ...) do
            parts[index] = tostring(select(index, ...))
        end

        DEFAULT_CHAT_FRAME:AddMessage(table.concat(parts, " "))
    end
end

BINDING_HEADER_GROUPSWAPPER = "GroupSwapper"
BINDING_NAME_GROUPSWAPPER_CAPTURE_TARGET = "Capture or swap current target"

local function DebugPrint(msg)
    if debugEnabled then
        print(printPrefix .. "DEBUG " .. msg)
    end
end

local function PrintHelp()
    local currentSwapper = swapperName or "Not set"
    local targetSwapTimeout = GetTargetSwapTimeout()

    print(printPrefix .. colorTitle .. "Beef's Group Swapper" .. colorReset)
    print(printPrefix .. colorHeader .. "Current swapper:" .. colorReset .. " " .. colorValue .. currentSwapper .. colorReset)
    print(printPrefix .. colorHeader .. "Workflow" .. colorReset)
    print(printPrefix .. "  1. Use " .. colorCommand .. "/setswapper <name>" .. colorReset .. " to choose the raid member who will execute swaps.")
    print(printPrefix .. "  2. Use " .. colorCommand .. "/gs" .. colorReset .. " to create or edit saved swap sets.")
    print(printPrefix .. "  3. In raid, the mini bar appears automatically when a swapper is set and at least one set exists.")
    print(printPrefix .. "  4. Press a mini-bar button to run that saved set through " .. colorCommand .. "/sswap" .. colorReset .. ".")
    print(printPrefix .. "  5. For ad hoc swaps, target player A, press your GroupSwapper target keybind, target player B, then press it again within " .. colorValue .. tostring(targetSwapTimeout) .. "s" .. colorReset .. ".")
    print(printPrefix .. "  6. Saved sets skip dead, ghost, offline, missing, or benched players outside groups 1-" .. colorValue .. tostring(maxActiveSwapGroup) .. colorReset .. " per pair. Manual " .. colorCommand .. "/swap" .. colorReset .. " and " .. colorCommand .. "/sswap" .. colorReset .. " do not.")
    print(printPrefix .. colorHeader .. "Commands" .. colorReset)
    print(printPrefix .. "  " .. colorCommand .. "/gs" .. colorReset .. " - open the config window")
    print(printPrefix .. "  " .. colorCommand .. "/swapper" .. colorReset .. " - show the current swapper")
    print(printPrefix .. "  " .. colorCommand .. "/setswapper <name>" .. colorReset .. " - set the current swapper")
    print(printPrefix .. "  " .. colorCommand .. "/swap <player1> <player2>" .. colorReset .. " - swap two raid members directly")
    print(printPrefix .. "  " .. colorCommand .. "/sswap <player1> <player2>" .. colorReset .. " - send a swap request to the configured swapper")
    print(printPrefix .. "  " .. colorCommand .. "/gstarget" .. colorReset .. " - capture or swap your current target")
    print(printPrefix .. "  " .. colorCommand .. "/gsbar" .. colorReset .. " - toggle the mini saved-set bar")
    print(printPrefix .. "  " .. colorCommand .. "/gsdebug [on|off]" .. colorReset .. " - toggle debug logging")
    print(printPrefix .. "  " .. colorCommand .. "/gshelp" .. colorReset .. " - show this help")
end

local function RegisterAddonPrefix(prefix)
    if C_ChatInfo and C_ChatInfo.RegisterAddonMessagePrefix then
        return C_ChatInfo.RegisterAddonMessagePrefix(prefix)
    elseif RegisterAddonMessagePrefix then
        return RegisterAddonMessagePrefix(prefix)
    end
end

local function SendRaidAddonMessage(prefix, msg)
    if C_ChatInfo and C_ChatInfo.SendAddonMessage then
        return C_ChatInfo.SendAddonMessage(prefix, msg, "RAID")
    elseif SendAddonMessage then
        return SendAddonMessage(prefix, msg, "RAID")
    end
end

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

local function ClearPendingTargetSwap()
    pendingTargetSwapName = nil
    pendingTargetSwapTime = nil
end

local function GetGroupMemberCount()
    if GetNumGroupMembers then
        return GetNumGroupMembers()
    elseif GetNumRaidMembers then
        return GetNumRaidMembers()
    end
    return 0
end

local function GetPlayerRaidIndex()
    local playerName = UnitName("player")
    local unitRaidIndex = UnitInRaid("player")

    for i = 1, GetGroupMemberCount() do
        local raidName = GetRaidRosterInfo(i)
        if raidName == playerName then
            return i
        end
    end

    return unitRaidIndex
end

local function PlayerIsInRaid()
    if IsInRaid then
        return IsInRaid()
    end
    return GetPlayerRaidIndex() ~= nil
end

local function PlayerCanManageRaid()
    if UnitIsGroupLeader and UnitIsGroupAssistant then
        local isLeader = UnitIsGroupLeader("player")
        local isAssistant = UnitIsGroupAssistant("player")
        DebugPrint("PlayerCanManageRaid modern API: name=" .. tostring(UnitName("player")) .. ", leader=" .. tostring(isLeader) .. ", assistant=" .. tostring(isAssistant))
        return isLeader or isAssistant
    end

    local raidIndex = GetPlayerRaidIndex()
    if raidIndex then
        local name, rank, subgroup = GetRaidRosterInfo(raidIndex)
        DebugPrint("PlayerCanManageRaid fallback: player=" .. tostring(UnitName("player")) .. ", unitInRaid=" .. tostring(UnitInRaid("player")) .. ", raidIndex=" .. tostring(raidIndex) .. ", rosterName=" .. tostring(name) .. ", rank=" .. tostring(rank) .. ", subgroup=" .. tostring(subgroup))
        return rank and rank > 0
    end

    DebugPrint("PlayerCanManageRaid fallback: player=" .. tostring(UnitName("player")) .. ", raidIndex=nil")

    return false
end

RegisterAddonPrefix(addonPrefix)

_G.swapperName = Trim(_G.swapperName)
if _G.swapperName == "" then
    _G.swapperName = nil
end
_G.GroupSwapperDB = _G.GroupSwapperDB or {}

local function EnsureDatabase()
    local db = _G.GroupSwapperDB
    if type(db) ~= "table" then
        db = {}
        _G.GroupSwapperDB = db
    end

    if type(db.swapSets) ~= "table" then
        db.swapSets = {}
    end

    if type(db.selectedSwapSet) ~= "number" then
        db.selectedSwapSet = 1
    end

    if type(db.buttonBarPosition) ~= "table" then
        db.buttonBarPosition = {
            point = "CENTER",
            relativePoint = "CENTER",
            x = 0,
            y = 220
        }
    end

    if type(db.buttonBarHidden) ~= "boolean" then
        db.buttonBarHidden = false
    end

    if type(db.targetSwapTimeout) ~= "number" then
        db.targetSwapTimeout = 2
    end

    if db.targetSwapTimeout < 1 then
        db.targetSwapTimeout = 1
    elseif db.targetSwapTimeout > 5 then
        db.targetSwapTimeout = 5
    end

    return db
end

local function GetDatabase()
    return EnsureDatabase()
end

local function GetSwapSets()
    return GetDatabase().swapSets
end

local function GetButtonBarPosition()
    return GetDatabase().buttonBarPosition
end

GetTargetSwapTimeout = function()
    return GetDatabase().targetSwapTimeout
end

local function SetTargetSwapTimeout(seconds)
    local db = GetDatabase()
    db.targetSwapTimeout = seconds
end

local function IsButtonBarHidden()
    return GetDatabase().buttonBarHidden
end

local function SetButtonBarHidden(hidden)
    GetDatabase().buttonBarHidden = hidden and true or false
end

local function NormalizeSelectedSwapSet()
    local db = GetDatabase()
    local swapSets = db.swapSets

    if #swapSets == 0 then
        db.selectedSwapSet = 0
    elseif db.selectedSwapSet < 1 or db.selectedSwapSet > #swapSets then
        db.selectedSwapSet = 1
    end

    return db.selectedSwapSet
end

local function GetSelectedSwapSet()
    local index = NormalizeSelectedSwapSet()
    if index == 0 then
        return nil, 0
    end

    return GetSwapSets()[index], index
end

local function CreateEmptySwapSet()
    return {
        name = "",
        pairs = {}
    }
end

local function NormalizeSwapSet(setData)
    if type(setData) ~= "table" then
        setData = CreateEmptySwapSet()
    end

    setData.name = Trim(setData.name)
    if type(setData.pairs) ~= "table" then
        setData.pairs = {}
    end

    local normalizedPairs = {}
    for _, pair in ipairs(setData.pairs) do
        if type(pair) == "table" then
            local player1 = Trim(pair.player1)
            local player2 = Trim(pair.player2)
            if player1 ~= "" and player2 ~= "" then
                table.insert(normalizedPairs, { player1 = player1, player2 = player2 })
            end
        end
    end
    setData.pairs = normalizedPairs

    return setData
end

local function NormalizeDatabase()
    local db = GetDatabase()
    local normalizedSets = {}

    for _, setData in ipairs(db.swapSets) do
        setData = NormalizeSwapSet(setData)
        if setData.name ~= "" then
            table.insert(normalizedSets, setData)
        end
    end

    db.swapSets = normalizedSets
    NormalizeSelectedSwapSet()
    return db
end

NormalizeDatabase()

------------------------------------------------------------
-- Swap Functions
------------------------------------------------------------

-- Returns the raid roster index for a given player name.
function GetRaidIndexByName(name)
    for i = 1, GetGroupMemberCount() do
        local raidName = GetRaidRosterInfo(i)        
        if raidName and raidName == name then
            return i
        end
    end
    return nil
end

local function SetStatusText(message, isError)
    if not configStatusText then
        return
    end

    configStatusText:SetText(message or "")
    if isError then
        configStatusText:SetTextColor(1.0, 0.2, 0.2)
    else
        configStatusText:SetTextColor(0.9, 0.82, 0.0)
    end
end

local function RefreshConfigWindow()
    if not configFrame then
        RefreshButtonBar()
        return
    end

    if configSwapperValue then
        configSwapperValue:SetText(swapperName or "Not set")
    end

    if configTargetSwapTimeoutValue then
        configTargetSwapTimeoutValue:SetText(GetTargetSwapTimeout() .. "s")
    end

    RefreshButtonBar()
end

local function SetSelectedSwapSet(index)
    local db = GetDatabase()
    db.selectedSwapSet = index or 0
    NormalizeSelectedSwapSet()
end

local function ClearSetEditorInputs()
    if configSetNameEditBox then
        configSetNameEditBox:SetText("")
    end

    for _, row in ipairs(configSetPairRows) do
        row.player1:SetText("")
        row.player2:SetText("")
    end
end

local function LoadSelectedSetIntoEditor()
    local selectedSet, index = GetSelectedSwapSet()

    if configSelectedSetValue then
        if selectedSet then
            configSelectedSetValue:SetText(index .. ". " .. selectedSet.name)
        else
            configSelectedSetValue:SetText("None")
        end
    end

    if not selectedSet then
        ClearSetEditorInputs()
        return
    end

    if configSetNameEditBox then
        configSetNameEditBox:SetText(selectedSet.name or "")
    end

    for rowIndex, row in ipairs(configSetPairRows) do
        local pair = selectedSet.pairs[rowIndex]
        row.player1:SetText(pair and pair.player1 or "")
        row.player2:SetText(pair and pair.player2 or "")
    end

end

local function RefreshSetButtons()
    for index, button in ipairs(configSetButtons) do
        local swapSet = GetSwapSets()[index]
        local selectedIndex = NormalizeSelectedSwapSet()

        if swapSet then
            button:SetText(index .. ". " .. swapSet.name)
            button:Show()
            if index == selectedIndex then
                button:LockHighlight()
            else
                button:UnlockHighlight()
            end
        else
            button:SetText("")
            button:Hide()
            button:UnlockHighlight()
        end
    end
end

local function RefreshSavedSetEditor()
    RefreshSetButtons()
    LoadSelectedSetIntoEditor()
    RefreshButtonBar()
end

local function HasConfiguredSwapper()
    return swapperName and swapperName ~= ""
end

local function GetRaidUnitByName(name)
    local index = GetRaidIndexByName(name)
    if not index then
        return nil, nil
    end

    return "raid" .. index, index
end

local function GetSetSwapAvailability(name)
    local unit, index = GetRaidUnitByName(name)
    if not unit or not index then
        return false, "not in the raid"
    end

    if UnitExists and not UnitExists(unit) then
        return false, "not available"
    end

    if UnitIsConnected and not UnitIsConnected(unit) then
        return false, "offline"
    end

    if UnitIsDead and UnitIsDead(unit) then
        return false, "dead"
    end

    if UnitIsGhost and UnitIsGhost(unit) then
        return false, "ghost"
    end

    local _, _, subgroup = GetRaidRosterInfo(index)
    if not subgroup or subgroup < 1 or subgroup > maxActiveSwapGroup then
        return false, "outside groups 1-" .. tostring(maxActiveSwapGroup)
    end

    return true, nil
end

local function ShouldShowButtonBar()
    return not IsButtonBarHidden() and PlayerIsInRaid() and HasConfiguredSwapper() and #GetSwapSets() > 0
end

local function SetButtonBarStatusText(message, isError)
    if not buttonBarStatusText then
        return
    end

    buttonBarStatusText:SetText(message or "")
    if isError then
        buttonBarStatusText:SetTextColor(1.0, 0.2, 0.2)
    else
        buttonBarStatusText:SetTextColor(0.9, 0.82, 0.0)
    end
end

local function UpdateButtonBarLayout()
    if not buttonBarFrame then
        return
    end

    local visibleCount = 0
    for index, button in ipairs(buttonBarButtons) do
        local swapSet = GetSwapSets()[index]
        if swapSet then
            visibleCount = visibleCount + 1
            button:ClearAllPoints()
            button:SetPoint("TOP", 0, -16 - ((visibleCount - 1) * 20))
        end
    end

    local buttonBarHeight = 20 + (visibleCount * 20)
    if visibleCount == 0 then
        buttonBarHeight = 20
    end

    buttonBarFrame:SetHeight(buttonBarHeight)
end

RefreshButtonBar = function()
    if not buttonBarFrame then
        return
    end

    for index, button in ipairs(buttonBarButtons) do
        local swapSet = GetSwapSets()[index]
        if swapSet then
            button:SetText(swapSet.name)
            button:Show()
        else
            button:SetText("")
            button:Hide()
        end
    end

    UpdateButtonBarLayout()

    if ShouldShowButtonBar() then
        buttonBarFrame:Show()
        if buttonBarStatusText and buttonBarStatusText:GetText() == "" then
            SetButtonBarStatusText("Ready.", false)
        end
    else
        buttonBarFrame:Hide()
    end
end

-- Swaps two raid members after validating permissions and state.
function Swap(player1, player2, options)
    options = options or {}
    DebugPrint("Swap request: executor=" .. tostring(UnitName("player")) .. ", configuredSwapper=" .. tostring(swapperName) .. ", player1=" .. tostring(player1) .. ", player2=" .. tostring(player2) .. ", unitInRaid=" .. tostring(UnitInRaid("player")) .. ", resolvedRaidIndex=" .. tostring(GetPlayerRaidIndex()) .. ", inCombat=" .. tostring(UnitAffectingCombat("player")))
    if UnitAffectingCombat("player") then
        print(printPrefix, "Cannot execute the swap while in combat.")
        SetStatusText("Cannot execute swaps while in combat.", true)
        SetButtonBarStatusText("Cannot execute swaps while in combat.", true)
        return false
    elseif not PlayerCanManageRaid() then
        print(printPrefix, "You do not have permission to perform swaps.")
        SetStatusText("You do not have permission to perform swaps.", true)
        SetButtonBarStatusText("You do not have permission to perform swaps.", true)
        return false
    elseif not (GetRaidIndexByName(player1) and GetRaidIndexByName(player2)) then
        print(printPrefix, "One or both players are not in the raid.")
        SetStatusText("One or both players are not in the raid.", true)
        SetButtonBarStatusText("One or both players are not in the raid.", true)
        return false
    else
        if options.skipUnavailableForSet then
            local player1Available, player1Reason = GetSetSwapAvailability(player1)
            local player2Available, player2Reason = GetSetSwapAvailability(player2)

            if not player1Available or not player2Available then
                local skipReason
                if not player1Available then
                    skipReason = player1 .. " is " .. player1Reason
                else
                    skipReason = player2 .. " is " .. player2Reason
                end

                print(printPrefix .. "Skipped set pair: " .. player1 .. " <-> " .. player2 .. " because " .. skipReason .. ".")
                SetStatusText("Skipped " .. player1 .. " <-> " .. player2 .. ": " .. skipReason .. ".", true)
                SetButtonBarStatusText("Skipped " .. player1 .. " <-> " .. player2 .. ": " .. skipReason .. ".", true)
                return false, "skipped"
            end
        end

        local idx1 = GetRaidIndexByName(player1)
        local idx2 = GetRaidIndexByName(player2)
        local _, _, grp1 = GetRaidRosterInfo(idx1)
        local _, _, grp2 = GetRaidRosterInfo(idx2)

        if grp1 == grp2 then
            print(printPrefix, "Both players are already in the same group.")
            SetStatusText("Both players are already in the same group.", true)
            SetButtonBarStatusText("Both players are already in the same group.", true)
            return false
        else
            SwapRaidSubgroup(idx1, idx2)
            print(printPrefix, "Swap completed:", player1, "from group", grp1, "<->", player2, "from group", grp2)
            SetStatusText("Swap completed for " .. player1 .. " and " .. player2 .. ".", false)
            SetButtonBarStatusText("Swap completed: " .. player1 .. " <-> " .. player2 .. ".", false)
            return true
        end
    end
end

-- Sends a swap request to the designated swapper via addon message.
function SendSwap(player1, player2, options)
    options = options or {}
    if not swapperName or swapperName == "" then
        print(printPrefix, "No swapper is configured.")
        SetStatusText("No swapper is configured.", true)
        SetButtonBarStatusText("No swapper is configured.", true)
        return false
    end

    if not SendAddonMessage and not (C_ChatInfo and C_ChatInfo.SendAddonMessage) then
        print(printPrefix, "Error: SendAddonMessage is not available.")
        SetStatusText("Addon messaging is not available.", true)
        SetButtonBarStatusText("Addon messaging is not available.", true)
        return false
    end

    local payload = player1 .. ";" .. player2
    if options.isSetSwap then
        payload = setSwapMessagePrefix .. payload
    end

    SendRaidAddonMessage(addonPrefix, payload)
    print(printPrefix, "Swap request sent: " .. player1 .. " <-> " .. player2)
    SetStatusText("Sent /sswap for " .. player1 .. " and " .. player2 .. ".", false)
    if options.isSetSwap then
        SetButtonBarStatusText("Queued set pair " .. player1 .. " <-> " .. player2 .. ".", false)
    else
        SetButtonBarStatusText("Sent /sswap for " .. player1 .. " and " .. player2 .. ".", false)
    end
    return true
end

_G.SendSwap = SendSwap

local function CaptureOrSwapCurrentTarget()
    -- Prefer mouseover for faster raid-frame picks, with target kept as a fallback.
    local unit = nil
    if UnitExists("mouseover") then
        unit = "mouseover"
    elseif UnitExists("target") then
        unit = "target"
    end

    if not unit then
        ClearPendingTargetSwap()
        print(printPrefix .. "You need a mouseover or player target first.")
        SetStatusText("You need a mouseover or player target first.", true)
        return false
    end

    local targetName = Trim(UnitName(unit))
    if targetName == "" then
        ClearPendingTargetSwap()
        print(printPrefix .. "Could not resolve the current target.")
        SetStatusText("Could not resolve the current target.", true)
        return false
    end

    if not GetRaidIndexByName(targetName) then
        ClearPendingTargetSwap()
        print(printPrefix .. "Target '" .. targetName .. "' is not in the raid.")
        SetStatusText("Target '" .. targetName .. "' is not in the raid.", true)
        return false
    end

    local now = GetTime and GetTime() or 0
    local timeout = GetTargetSwapTimeout()
    if pendingTargetSwapName and pendingTargetSwapTime and (now - pendingTargetSwapTime) <= timeout then
        local firstTarget = pendingTargetSwapName

        if firstTarget == targetName then
            pendingTargetSwapTime = now
            print(printPrefix .. "Stored target is already " .. targetName .. ". Target a different player for the second pick.")
            SetStatusText("Stored " .. targetName .. ". Target a different player next.", true)
            return false
        end

        ClearPendingTargetSwap()
        if SendSwap(firstTarget, targetName) then
            print(printPrefix .. "Target swap sent: " .. firstTarget .. " <-> " .. targetName)
            SetStatusText("Sent target swap " .. firstTarget .. " <-> " .. targetName .. ".", false)
            return true
        end

        return false
    end

    pendingTargetSwapName = targetName
    pendingTargetSwapTime = now
    print(printPrefix .. "Stored target: " .. targetName .. ". Target another raid member and trigger again within " .. timeout .. "s.")
    SetStatusText("Stored target " .. targetName .. ".", false)
    return true
end

function GroupSwapper_CaptureTargetBinding()
    CaptureOrSwapCurrentTarget()
end

------------------------------------------------------------
-- Swap Queue Functions
------------------------------------------------------------

local swapQueue = {}
local isSwapping = false    -- Prevent overlapping swaps while raid indices are changing.
local swapDelay = 0.5
local swapTimerFrame = CreateFrame("Frame")
local swapTimeRemaining = nil

local function StartSwapDelay(delay)
    swapTimeRemaining = delay
    swapTimerFrame:SetScript("OnUpdate", function(self, elapsed)
        swapTimeRemaining = swapTimeRemaining - elapsed
        if swapTimeRemaining <= 0 then
            self:SetScript("OnUpdate", nil)
            swapTimeRemaining = nil
            isSwapping = false
            TryProcessQueue()
        end
    end)
end

function QueueSwap(player1, player2, options)
    table.insert(swapQueue, {player1 = player1, player2 = player2, options = options})
    TryProcessQueue()
end

-- Processes the queue with a delay; stops on empty queue or combat.
function TryProcessQueue()
    if isSwapping or InCombatLockdown() then return end
    if #swapQueue == 0 then return end

    isSwapping = true
    local pair = table.remove(swapQueue, 1)
    Swap(pair.player1, pair.player2, pair.options)
    StartSwapDelay(swapDelay)
end

------------------------------------------------------------
-- Events and Communication
------------------------------------------------------------


-- Handles received addon messages.
local function OnAddonMessage(prefix, msg, _, sender)
    if prefix ~= addonPrefix then return end

    -- 1. Swapper configuration message
    if msg:find("^SET_SWAPPER:") then
        local newName = msg:sub(13)
                       
        swapperName = newName
        print(printPrefix .. "Swapper updated by " .. sender .. " -> " .. newName)
        RefreshConfigWindow()
        
        
    end

    -- 2. Swap commands, executed only by the configured swapper
    if UnitName("player") ~= swapperName then return end

    local options
    if msg:find("^" .. setSwapMessagePrefix) then
        msg = msg:sub(string.len(setSwapMessagePrefix) + 1)
        options = { skipUnavailableForSet = true }
    end

    local player1, player2 = strsplit(";", msg)
    if player1 and player2 then
        if not InCombatLockdown() then
            print(printPrefix .. "Received swap from " .. sender .. ": " .. player1 .. " <-> " .. player2)
            QueueSwap(player1, player2, options)
        else
            print(printPrefix .. "Received swap, but I am in combat. Ignored by the swapper.")
        end
    end
    
end

-- Broadcasts the current swapper to the raid; only leader/assist should send it.
local function BroadcastSwapper()
    local i = GetPlayerRaidIndex()
    if i then
        local _, rank = GetRaidRosterInfo(i)
        if rank > 0 then
            SendRaidAddonMessage(addonPrefix, "SET_SWAPPER:" .. swapperName)
        end
    end
end

-- Frame that listens for addon messages and raid roster state changes.

local GSFrame = CreateFrame("Frame")
GSFrame:RegisterEvent("CHAT_MSG_ADDON")
GSFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
GSFrame:RegisterEvent("GROUP_ROSTER_UPDATE")
GSFrame:RegisterEvent("RAID_ROSTER_UPDATE")

local wasInRaid = false  -- Tracks transitions into a raid.

GSFrame:SetScript("OnEvent", function(self, event, ...)
    if event == "CHAT_MSG_ADDON" then
        local prefix, message, channel, sender = ...
        OnAddonMessage(prefix, message, channel, sender)

    elseif event == "PLAYER_ENTERING_WORLD" or event == "GROUP_ROSTER_UPDATE" or event == "RAID_ROSTER_UPDATE" then
        CreateButtonBar()
        local inRaid = PlayerIsInRaid()

        if inRaid and not wasInRaid then
            
            print(printPrefix .. "Current swapper: " .. (swapperName or "Not set"))
        end

        wasInRaid = inRaid
        RefreshConfigWindow()
    end
end)

------------------------------------------------------------
-- Slash Commands
------------------------------------------------------------

function SetSwapper(name)
    local myIndex = GetPlayerRaidIndex()
    if not myIndex then
        print(printPrefix .. "You must be in a raid to assign a swapper.")
        SetStatusText("You must be in a raid to assign a swapper.", true)
        return false
    end

    local _, rank = GetRaidRosterInfo(myIndex)
    if rank == 0 then
        print(printPrefix .. "Only the raid leader or assistants can change the swapper.")
        SetStatusText("Only leader or assistants can change the swapper.", true)
        return false
    end

    for i = 1, GetGroupMemberCount() do
        local raidName = GetRaidRosterInfo(i)
        if raidName == name then
            -- Set the swapper only after raid membership and permissions are verified.
            swapperName = name
            print(printPrefix .. "Swapper set locally to: " .. name)
            BroadcastSwapper()
            RefreshConfigWindow()
            SetStatusText("Swapper set to " .. name .. ".", false)
            return true
        end
    end

    print(printPrefix .. "Player '" .. name .. "' is not in the raid.")
    SetStatusText("Player '" .. name .. "' is not in the raid.", true)
    return false
end

local function CreateLabeledEditBox(parent, labelText, width, anchor, relativeTo, relativePoint, xOffset, yOffset)
    local label = parent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    label:SetPoint(anchor, relativeTo, relativePoint, xOffset, yOffset)
    label:SetText(labelText)

    local editBox = CreateFrame("EditBox", nil, parent)
    editBox:SetAutoFocus(false)
    editBox:SetWidth(width)
    editBox:SetHeight(20)
    editBox:SetPoint("TOPLEFT", label, "BOTTOMLEFT", 0, -6)
    editBox:SetMaxLetters(24)
    editBox:SetFontObject(ChatFontNormal)
    editBox:SetTextInsets(6, 6, 0, 0)
    editBox:SetBackdrop({
        bgFile = "Interface\\ChatFrame\\ChatFrameBackground",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true,
        tileSize = 16,
        edgeSize = 12,
        insets = { left = 2, right = 2, top = 2, bottom = 2 }
    })
    editBox:SetBackdropColor(0.02, 0.02, 0.02, 0.85)
    editBox:SetBackdropBorderColor(0.28, 0.28, 0.28, 0.9)

    return editBox, label
end

local function CreateStyledEditBox(parent, width, height, maxLetters)
    local editBox = CreateFrame("EditBox", nil, parent)
    editBox:SetAutoFocus(false)
    editBox:SetWidth(width)
    editBox:SetHeight(height or 20)
    editBox:SetMaxLetters(maxLetters or 24)
    editBox:SetFontObject(ChatFontNormal)
    editBox:SetTextInsets(6, 6, 0, 0)
    editBox:SetBackdrop({
        bgFile = "Interface\\ChatFrame\\ChatFrameBackground",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true,
        tileSize = 16,
        edgeSize = 12,
        insets = { left = 2, right = 2, top = 2, bottom = 2 }
    })
    editBox:SetBackdropColor(0.02, 0.02, 0.02, 0.85)
    editBox:SetBackdropBorderColor(0.28, 0.28, 0.28, 0.9)
    return editBox
end

local function ReadManualSwapInputs()
    local player1 = Trim(configPlayerOneEditBox and configPlayerOneEditBox:GetText())
    local player2 = Trim(configPlayerTwoEditBox and configPlayerTwoEditBox:GetText())

    if player1 == "" or player2 == "" then
        SetStatusText("Enter two player names first.", true)
        return nil, nil
    end

    return player1, player2
end

local function FillManualPlayer(editBox, slotLabel)
    if not editBox then
        return
    end

    if not UnitExists("target") then
        SetStatusText("You need a target to fill " .. slotLabel .. ".", true)
        return
    end

    local targetName = UnitName("target")
    if not targetName or targetName == "" then
        SetStatusText("Could not resolve the target name.", true)
        return
    end

    editBox:SetText(targetName)
    SetStatusText(slotLabel .. " set to " .. targetName .. ".", false)
end

local function FillSetPairFromTarget(row)
    if not row then
        return
    end

    if not UnitExists("target") then
        SetStatusText("You need a target selected first.", true)
        return
    end

    local targetName = UnitName("target")
    if not targetName or targetName == "" then
        SetStatusText("Could not resolve the target name.", true)
        return
    end

    if Trim(row.player1:GetText()) == "" then
        row.player1:SetText(targetName)
        SetStatusText("Filled pair " .. row.index .. " player 1.", false)
        return
    end

    row.player2:SetText(targetName)
    SetStatusText("Filled pair " .. row.index .. " player 2.", false)
end

local function CollectSetEditorData()
    local setName = Trim(configSetNameEditBox and configSetNameEditBox:GetText())
    local pairs = {}

    if setName == "" then
        SetStatusText("Enter a set name first.", true)
        return nil
    end

    for _, row in ipairs(configSetPairRows) do
        local player1 = Trim(row.player1:GetText())
        local player2 = Trim(row.player2:GetText())

        if player1 ~= "" or player2 ~= "" then
            if player1 == "" or player2 == "" then
                SetStatusText("Each saved pair needs both player names.", true)
                return nil
            end

            table.insert(pairs, { player1 = player1, player2 = player2 })
        end
    end

    if #pairs == 0 then
        SetStatusText("Add at least one player pair to save a set.", true)
        return nil
    end

    return {
        name = setName,
        pairs = pairs
    }
end

local function CreateSwapSetFromEditor()
    local setData = CollectSetEditorData()
    if not setData then
        return
    end

    local swapSets = GetSwapSets()
    if #swapSets >= maxConfigSetButtons then
        SetStatusText("This first UI pass supports up to " .. maxConfigSetButtons .. " saved sets.", true)
        return
    end

    table.insert(swapSets, NormalizeSwapSet(setData))
    SetSelectedSwapSet(#swapSets)
    RefreshSavedSetEditor()
    SetStatusText("Saved new set '" .. setData.name .. "'.", false)
end

local function UpdateSelectedSwapSetFromEditor()
    local selectedSet, index = GetSelectedSwapSet()
    if not selectedSet then
        SetStatusText("Select a saved set first.", true)
        return
    end

    local setData = CollectSetEditorData()
    if not setData then
        return
    end

    GetSwapSets()[index] = NormalizeSwapSet(setData)
    RefreshSavedSetEditor()
    SetStatusText("Updated set '" .. setData.name .. "'.", false)
end

local function DeleteSelectedSwapSet()
    local _, index = GetSelectedSwapSet()
    if index == 0 then
        SetStatusText("There is no saved set to delete.", true)
        return
    end

    local swapSets = GetSwapSets()
    local deletedName = swapSets[index].name
    table.remove(swapSets, index)

    if #swapSets == 0 then
        SetSelectedSwapSet(0)
    elseif index > #swapSets then
        SetSelectedSwapSet(#swapSets)
    else
        SetSelectedSwapSet(index)
    end

    RefreshSavedSetEditor()
    SetStatusText("Deleted set '" .. deletedName .. "'.", false)
end

local function DeleteAllSwapSets()
    local swapSets = GetSwapSets()
    if #swapSets == 0 then
        SetStatusText("There are no saved sets to delete.", true)
        return
    end

    for index = #swapSets, 1, -1 do
        table.remove(swapSets, index)
    end

    SetSelectedSwapSet(0)
    RefreshSavedSetEditor()
    SetStatusText("Deleted all saved sets.", false)
end

local function RunSwapSet(setData)
    if not setData then
        SetStatusText("Select a saved set first.", true)
        SetButtonBarStatusText("Select a saved set first.", true)
        return false
    end

    if not setData.pairs or #setData.pairs == 0 then
        SetStatusText("The selected set has no saved pairs.", true)
        SetButtonBarStatusText("The selected set has no saved pairs.", true)
        return false
    end

    local sentCount = 0
    for _, pair in ipairs(setData.pairs) do
        local player1Available, player1Reason = GetSetSwapAvailability(pair.player1)
        local player2Available, player2Reason = GetSetSwapAvailability(pair.player2)

        if not player1Available or not player2Available then
            local skipReason
            if not player1Available then
                skipReason = pair.player1 .. " is " .. player1Reason
            else
                skipReason = pair.player2 .. " is " .. player2Reason
            end

            print(printPrefix .. "Skipped set pair before send: " .. pair.player1 .. " <-> " .. pair.player2 .. " because " .. skipReason .. ".")
            SetStatusText("Skipped " .. pair.player1 .. " <-> " .. pair.player2 .. ": " .. skipReason .. ".", true)
            SetButtonBarStatusText("Skipped " .. pair.player1 .. " <-> " .. pair.player2 .. ": " .. skipReason .. ".", true)
        elseif SendSwap(pair.player1, pair.player2, { isSetSwap = true }) then
            sentCount = sentCount + 1
        else
            return false
        end
    end

    if sentCount == 0 then
        SetStatusText("Set '" .. setData.name .. "' sent no swaps.", true)
        SetButtonBarStatusText("Set '" .. setData.name .. "' sent no swaps.", true)
        return false
    end

    SetStatusText("Ran set '" .. setData.name .. "' with " .. sentCount .. " swap(s).", false)
    SetButtonBarStatusText("Ran set '" .. setData.name .. "' with " .. sentCount .. " swap(s).", false)
    return true
end

local function RunSelectedSwapSet()
    local selectedSet = GetSelectedSwapSet()
    return RunSwapSet(selectedSet)
end

local function ConfirmDeleteAllSwapSets()
    if not StaticPopupDialogs then
        DeleteAllSwapSets()
        return
    end

    StaticPopupDialogs["GROUPSWAPPER_DELETE_ALL_SETS"] = {
        text = "Delete all saved swap sets?",
        button1 = ACCEPT,
        button2 = CANCEL,
        OnAccept = function()
            DeleteAllSwapSets()
        end,
        timeout = 0,
        whileDead = 1,
        hideOnEscape = 1,
        preferredIndex = 3
    }

    StaticPopup_Show("GROUPSWAPPER_DELETE_ALL_SETS")
end

CreateButtonBar = function()
    if buttonBarFrame then
        return
    end

    buttonBarFrame = CreateFrame("Frame", "GroupSwapperButtonBar", UIParent)
    buttonBarFrame:SetWidth(84)
    buttonBarFrame:SetHeight(20)
    buttonBarFrame:SetFrameStrata("MEDIUM")
    buttonBarFrame:SetToplevel(true)
    buttonBarFrame:EnableMouse(true)
    buttonBarFrame:SetMovable(true)
    buttonBarFrame:RegisterForDrag("LeftButton")
    buttonBarFrame:SetScript("OnDragStart", function(self)
        if InCombatLockdown and InCombatLockdown() then
            return
        end
        self:StartMoving()
    end)
    buttonBarFrame:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        local point, _, relativePoint, x, y = self:GetPoint(1)
        local position = GetButtonBarPosition()
        position.point = point or "CENTER"
        position.relativePoint = relativePoint or "CENTER"
        position.x = x or 0
        position.y = y or 220
    end)
    buttonBarFrame:SetBackdrop({
        bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true,
        tileSize = 16,
        edgeSize = 12,
        insets = { left = 1, right = 1, top = 2, bottom = 1 }
    })
    buttonBarFrame:SetBackdropColor(0.08, 0.08, 0.08, 0.9)

    local position = GetButtonBarPosition()
    buttonBarFrame:SetPoint(position.point or "CENTER", UIParent, position.relativePoint or "CENTER", position.x or 0, position.y or 220)

    local title = buttonBarFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    title:SetPoint("TOP", 0, -6)
    title:SetText("Swapper")

    for index = 1, maxConfigSetButtons do
        local button = CreateFrame("Button", nil, buttonBarFrame, "UIPanelButtonTemplate")
        button:SetWidth(68)
        button:SetHeight(18)
        button:SetPoint("TOP", 0, -16 - ((index - 1) * 20))
        button:SetText("")
        button:SetScript("OnClick", function()
            local swapSet = GetSwapSets()[index]
            if not swapSet then
                SetButtonBarStatusText("No set is saved in that slot.", true)
                return
            end

            SetSelectedSwapSet(index)
            RefreshSavedSetEditor()
            RunSwapSet(swapSet)
        end)
        buttonBarButtons[index] = button
    end

    RefreshButtonBar()
end

local function OpenConfigWindow()
    CreateButtonBar()

    if not configFrame then
        configFrame = CreateFrame("Frame", "GroupSwapperConfigFrame", UIParent)
        configFrame:SetWidth(590)
        configFrame:SetHeight(620)
        configFrame:SetPoint("CENTER")
        configFrame:SetFrameStrata("DIALOG")
        configFrame:SetToplevel(true)
        configFrame:EnableMouse(true)
        configFrame:SetMovable(true)
        configFrame:RegisterForDrag("LeftButton")
        configFrame:SetScript("OnDragStart", configFrame.StartMoving)
        configFrame:SetScript("OnDragStop", configFrame.StopMovingOrSizing)
        configFrame:SetBackdrop({
            bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
            edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
            tile = true,
            tileSize = 32,
            edgeSize = 32,
            insets = { left = 11, right = 12, top = 12, bottom = 11 }
        })
        configFrame:SetBackdropColor(1, 1, 1, 1)
        configFrame:Hide()

        table.insert(UISpecialFrames, "GroupSwapperConfigFrame")

        local title = configFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
        title:SetPoint("TOP", 0, -14)
        title:SetText("Beef's Group Swapper")

        local close = CreateFrame("Button", nil, configFrame, "UIPanelCloseButton")
        close:SetPoint("TOPRIGHT", -6, -6)

        local swapperLabel = configFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        swapperLabel:SetPoint("TOPLEFT", 20, -38)
        swapperLabel:SetText("Current swapper:")

        configSwapperValue = configFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        configSwapperValue:SetPoint("LEFT", swapperLabel, "RIGHT", 8, 0)
        configSwapperValue:SetText("")

        local meButton = CreateFrame("Button", nil, configFrame, "UIPanelButtonTemplate")
        meButton:SetWidth(120)
        meButton:SetHeight(22)
        meButton:SetPoint("TOPLEFT", 20, -64)
        meButton:SetText("Set Me")
        meButton:SetScript("OnClick", function()
            local playerName = UnitName("player")
            if not playerName then
                SetStatusText("Could not resolve your player name.", true)
                return
            end

            SetSwapper(playerName)
        end)

        local targetButton = CreateFrame("Button", nil, configFrame, "UIPanelButtonTemplate")
        targetButton:SetWidth(120)
        targetButton:SetHeight(22)
        targetButton:SetPoint("LEFT", meButton, "RIGHT", 8, 0)
        targetButton:SetText("Set Target")
        targetButton:SetScript("OnClick", function()
            if not UnitExists("target") then
                SetStatusText("You need a raid target selected.", true)
                return
            end

            local targetName = UnitName("target")
            if not targetName then
                SetStatusText("Could not resolve the target name.", true)
                return
            end

            SetSwapper(targetName)
        end)

        local refreshBarButton = CreateFrame("Button", nil, configFrame, "UIPanelButtonTemplate")
        refreshBarButton:SetWidth(120)
        refreshBarButton:SetHeight(22)
        refreshBarButton:SetPoint("LEFT", targetButton, "RIGHT", 8, 0)
        refreshBarButton:SetText("Refresh Bar")
        refreshBarButton:SetScript("OnClick", function()
            CreateButtonBar()
            RefreshButtonBar()
            SetStatusText("Refreshed the mini button bar.", false)
        end)

        local topDivider = configFrame:CreateTexture(nil, "ARTWORK")
        topDivider:SetTexture(1, 0.82, 0, 0.18)
        topDivider:SetPoint("TOPLEFT", 20, -94)
        topDivider:SetWidth(548)
        topDivider:SetHeight(1)

        local manualLabel = configFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        manualLabel:SetPoint("TOPLEFT", 20, -108)
        manualLabel:SetText("Manual swap test")

        configPlayerOneEditBox = CreateLabeledEditBox(configFrame, "Player 1", 120, "TOPLEFT", manualLabel, "BOTTOMLEFT", 0, -8)
        configPlayerTwoEditBox = CreateLabeledEditBox(configFrame, "Player 2", 120, "TOPLEFT", manualLabel, "BOTTOMLEFT", 160, -8)

        local playerOneTargetButton = CreateFrame("Button", nil, configFrame, "UIPanelButtonTemplate")
        playerOneTargetButton:SetWidth(120)
        playerOneTargetButton:SetHeight(20)
        playerOneTargetButton:SetPoint("TOPLEFT", configPlayerOneEditBox, "BOTTOMLEFT", 0, -8)
        playerOneTargetButton:SetText("Target -> P1")
        playerOneTargetButton:SetScript("OnClick", function()
            FillManualPlayer(configPlayerOneEditBox, "Player 1")
        end)

        local playerTwoTargetButton = CreateFrame("Button", nil, configFrame, "UIPanelButtonTemplate")
        playerTwoTargetButton:SetWidth(120)
        playerTwoTargetButton:SetHeight(20)
        playerTwoTargetButton:SetPoint("TOPLEFT", configPlayerTwoEditBox, "BOTTOMLEFT", 0, -8)
        playerTwoTargetButton:SetText("Target -> P2")
        playerTwoTargetButton:SetScript("OnClick", function()
            FillManualPlayer(configPlayerTwoEditBox, "Player 2")
        end)

        local directButton = CreateFrame("Button", nil, configFrame, "UIPanelButtonTemplate")
        directButton:SetWidth(120)
        directButton:SetHeight(22)
        directButton:SetPoint("TOPLEFT", configPlayerTwoEditBox, "TOPRIGHT", 16, 0)
        directButton:SetText("Direct /swap")
        directButton:SetScript("OnClick", function()
            local player1, player2 = ReadManualSwapInputs()
            if not player1 then
                return
            end

            Swap(player1, player2)
        end)

        local sendButton = CreateFrame("Button", nil, configFrame, "UIPanelButtonTemplate")
        sendButton:SetWidth(120)
        sendButton:SetHeight(22)
        sendButton:SetPoint("TOPLEFT", playerTwoTargetButton, "TOPRIGHT", 16, 0)
        sendButton:SetText("Send /sswap")
        sendButton:SetScript("OnClick", function()
            local player1, player2 = ReadManualSwapInputs()
            if not player1 then
                return
            end

            SendSwap(player1, player2)
        end)

        local bottomDivider = configFrame:CreateTexture(nil, "ARTWORK")
        bottomDivider:SetTexture(1, 0.82, 0, 0.18)
        bottomDivider:SetPoint("TOPLEFT", 20, -226)
        bottomDivider:SetWidth(548)
        bottomDivider:SetHeight(1)

        local savedSetsLabel = configFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        savedSetsLabel:SetPoint("TOPLEFT", 20, -242)
        savedSetsLabel:SetText("Saved swap sets")

        local selectedSetLabel = configFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        selectedSetLabel:SetPoint("TOPLEFT", 20, -264)
        selectedSetLabel:SetText("Selected set:")

        configSelectedSetValue = configFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        configSelectedSetValue:SetPoint("LEFT", selectedSetLabel, "RIGHT", 8, 0)
        configSelectedSetValue:SetText("None")

        for index = 1, maxConfigSetButtons do
            local setButton = CreateFrame("Button", nil, configFrame, "UIPanelButtonTemplate")
            setButton:SetWidth(180)
            setButton:SetHeight(20)
            setButton:SetPoint("TOPLEFT", 20, -272 - (index * 24))
            setButton:SetText("")
            setButton:SetScript("OnClick", function()
                SetSelectedSwapSet(index)
                RefreshSavedSetEditor()
                SetStatusText("Loaded saved set " .. index .. ".", false)
            end)
            configSetButtons[index] = setButton
        end

        local setNameLabel = configFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        setNameLabel:SetPoint("TOPLEFT", 228, -242)
        setNameLabel:SetText("Set name")

        configSetNameEditBox = CreateStyledEditBox(configFrame, 170, 20, 32)
        configSetNameEditBox:SetPoint("TOPLEFT", setNameLabel, "BOTTOMLEFT", 0, -6)

        local pairsLabel = configFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        pairsLabel:SetPoint("TOPLEFT", 228, -294)
        pairsLabel:SetText("Saved pairs")

        for index = 1, maxConfigSetRows do
            local rowTopOffset = -314 - ((index - 1) * 28)
            local pairLabel = configFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            pairLabel:SetPoint("TOPLEFT", 228, rowTopOffset)
            pairLabel:SetText(index .. ".")

            local player1Box = CreateStyledEditBox(configFrame, 72, 20, 24)
            player1Box:SetPoint("LEFT", pairLabel, "RIGHT", 8, 0)

            local arrow = configFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            arrow:SetPoint("LEFT", player1Box, "RIGHT", 6, 0)
            arrow:SetText("<->")

            local player2Box = CreateStyledEditBox(configFrame, 72, 20, 24)
            player2Box:SetPoint("LEFT", arrow, "RIGHT", 6, 0)

            local fillButton = CreateFrame("Button", nil, configFrame, "UIPanelButtonTemplate")
            fillButton:SetWidth(68)
            fillButton:SetHeight(20)
            fillButton:SetPoint("LEFT", player2Box, "RIGHT", 8, 0)
            fillButton:SetText("Target")

            local row = {
                index = index,
                player1 = player1Box,
                player2 = player2Box
            }
            fillButton:SetScript("OnClick", function()
                FillSetPairFromTarget(row)
            end)

            configSetPairRows[index] = row
        end

        local newSetButton = CreateFrame("Button", nil, configFrame, "UIPanelButtonTemplate")
        newSetButton:SetWidth(72)
        newSetButton:SetHeight(22)
        newSetButton:SetPoint("TOPLEFT", 228, -466)
        newSetButton:SetText("New Set")
        newSetButton:SetScript("OnClick", function()
            CreateSwapSetFromEditor()
        end)

        local updateSetButton = CreateFrame("Button", nil, configFrame, "UIPanelButtonTemplate")
        updateSetButton:SetWidth(72)
        updateSetButton:SetHeight(22)
        updateSetButton:SetPoint("LEFT", newSetButton, "RIGHT", 8, 0)
        updateSetButton:SetText("Update")
        updateSetButton:SetScript("OnClick", function()
            UpdateSelectedSwapSetFromEditor()
        end)

        local runSetButton = CreateFrame("Button", nil, configFrame, "UIPanelButtonTemplate")
        runSetButton:SetWidth(72)
        runSetButton:SetHeight(22)
        runSetButton:SetPoint("LEFT", updateSetButton, "RIGHT", 8, 0)
        runSetButton:SetText("Run Set")
        runSetButton:SetScript("OnClick", function()
            RunSelectedSwapSet()
        end)

        local deleteSetButton = CreateFrame("Button", nil, configFrame, "UIPanelButtonTemplate")
        deleteSetButton:SetWidth(72)
        deleteSetButton:SetHeight(22)
        deleteSetButton:SetPoint("LEFT", runSetButton, "RIGHT", 8, 0)
        deleteSetButton:SetText("Delete")
        deleteSetButton:SetScript("OnClick", function()
            DeleteSelectedSwapSet()
        end)

        local deleteAllSetsButton = CreateFrame("Button", nil, configFrame, "UIPanelButtonTemplate")
        deleteAllSetsButton:SetWidth(84)
        deleteAllSetsButton:SetHeight(22)
        deleteAllSetsButton:SetPoint("TOPLEFT", updateSetButton, "BOTTOMLEFT", 0, -8)
        deleteAllSetsButton:SetText("Delete All")
        deleteAllSetsButton:SetScript("OnClick", function()
            ConfirmDeleteAllSwapSets()
        end)

        local clearEditorButton = CreateFrame("Button", nil, configFrame, "UIPanelButtonTemplate")
        clearEditorButton:SetWidth(72)
        clearEditorButton:SetHeight(22)
        clearEditorButton:SetPoint("TOPLEFT", newSetButton, "BOTTOMLEFT", 0, -8)
        clearEditorButton:SetText("Clear")
        clearEditorButton:SetScript("OnClick", function()
            ClearSetEditorInputs()
            SetStatusText("Cleared the editor fields.", false)
        end)

        deleteAllSetsButton:SetPoint("LEFT", clearEditorButton, "RIGHT", 8, 0)

        local targetSwapDivider = configFrame:CreateTexture(nil, "ARTWORK")
        targetSwapDivider:SetTexture(1, 0.82, 0, 0.18)
        targetSwapDivider:SetPoint("TOPLEFT", 20, -524)
        targetSwapDivider:SetWidth(548)
        targetSwapDivider:SetHeight(1)

        local targetSwapLabel = configFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        targetSwapLabel:SetPoint("TOPLEFT", 20, -540)
        targetSwapLabel:SetText("Target swap")

        local targetSwapHelpText = configFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        targetSwapHelpText:SetPoint("TOPLEFT", targetSwapLabel, "BOTTOMLEFT", 0, -6)
        targetSwapHelpText:SetWidth(190)
        targetSwapHelpText:SetJustifyH("LEFT")
        targetSwapHelpText:SetText("Bind it in Key Bindings under GroupSwapper.")

        local targetSwapSlider = CreateFrame("Slider", "GroupSwapperTargetSwapTimeoutSlider", configFrame, "OptionsSliderTemplate")
        targetSwapSlider:SetWidth(170)
        targetSwapSlider:SetHeight(16)
        targetSwapSlider:SetPoint("TOPLEFT", 228, -546)
        targetSwapSlider:SetMinMaxValues(1, 5)
        targetSwapSlider:SetValueStep(1)
        if targetSwapSlider.SetObeyStepOnDrag then
            targetSwapSlider:SetObeyStepOnDrag(true)
        end
        targetSwapSlider:SetValue(GetTargetSwapTimeout())
        _G[targetSwapSlider:GetName() .. "Low"]:SetText("1s")
        _G[targetSwapSlider:GetName() .. "High"]:SetText("5s")
        _G[targetSwapSlider:GetName() .. "Text"]:SetText("Target swap timeout")
        targetSwapSlider:SetScript("OnValueChanged", function(self, value)
            local roundedValue = math.floor((value or 1) + 0.5)
            SetTargetSwapTimeout(roundedValue)
            if configTargetSwapTimeoutValue then
                configTargetSwapTimeoutValue:SetText(roundedValue .. "s")
            end
        end)

        configTargetSwapTimeoutValue = configFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        configTargetSwapTimeoutValue:SetPoint("LEFT", targetSwapSlider, "RIGHT", 12, 0)
        configTargetSwapTimeoutValue:SetText(GetTargetSwapTimeout() .. "s")

        configStatusText = configFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        configStatusText:SetPoint("BOTTOMLEFT", 20, 22)
        configStatusText:SetWidth(320)
        configStatusText:SetJustifyH("LEFT")
        configStatusText:SetText("")
    end

    RefreshConfigWindow()
    RefreshSavedSetEditor()
    SetStatusText("Ready.", false)
    configFrame:Show()
end

local function ToggleConfigWindow()
    if configFrame and configFrame:IsShown() then
        configFrame:Hide()
        return
    end

    OpenConfigWindow()
end

local function ToggleButtonBar()
    local hidden = not IsButtonBarHidden()
    SetButtonBarHidden(hidden)
    CreateButtonBar()
    RefreshButtonBar()

    if hidden then
        print(printPrefix .. "Mini bar hidden.")
        SetStatusText("Mini bar hidden.", false)
    else
        print(printPrefix .. "Mini bar shown when raid conditions are met.")
        SetStatusText("Mini bar shown when conditions are met.", false)
    end
end


SLASH_SETSWAPPER1 = "/setswapper"
SlashCmdList["SETSWAPPER"] = function(msg)
    local name = Trim(msg)
    if name ~= "" then
        SetSwapper(name)
    else
        print(printPrefix .. "Usage: /setswapper <name>")
    end
end

SLASH_SWAPPER1 = "/swapper"
SlashCmdList["SWAPPER"] = function(msg)
    msg = Trim(msg)
    if msg == "" then
        print(printPrefix .. colorHeader .. "Current swapper:" .. colorReset .. " " .. colorValue .. (swapperName or "Not set") .. colorReset)
    elseif msg == "help" then
        PrintHelp()
    else
        print(printPrefix .. colorHeader .. "Usage:" .. colorReset .. " " .. colorCommand .. "/swapper" .. colorReset)
        print(printPrefix .. "Use " .. colorCommand .. "/setswapper <name>" .. colorReset .. " to assign the swapper.")
    end
end

SLASH_SWAP1 = "/swap"
SlashCmdList["SWAP"] = function(msg)
    local de, a = msg:match("^(%S+)%s+(%S+)$")
    if de and a then
        Swap(de, a)
    else
        print(printPrefix .. "Usage: /swap <player1> <player2>")
    end
end


SLASH_SSWAP1 = "/sswap"
SlashCmdList["SSWAP"] = function(msg)
    local player1, player2 = msg:match("^(%S+)%s+(%S+)$")
    if player1 and player2 then
        SendSwap(player1, player2)
    else
        print(printPrefix .. "Usage: /sswap <player1> <player2>")
    end
end

SLASH_GSDEBUG1 = "/gsdebug"
SlashCmdList["GSDEBUG"] = function(msg)
    msg = Trim(msg):lower()
    if msg == "on" then
        debugEnabled = true
    elseif msg == "off" then
        debugEnabled = false
    else
        debugEnabled = not debugEnabled
    end

    print(printPrefix .. "Debug " .. (debugEnabled and "enabled" or "disabled"))
end

SLASH_GS1 = "/gs"
SLASH_GS2 = "/groupswapper"
SlashCmdList["GS"] = function()
    ToggleConfigWindow()
end

SLASH_GSBAR1 = "/gsbar"
SlashCmdList["GSBAR"] = function()
    ToggleButtonBar()
end

SLASH_GSTARGET1 = "/gstarget"
SlashCmdList["GSTARGET"] = function()
    CaptureOrSwapCurrentTarget()
end

SLASH_GSHELP1 = "/gshelp"
SlashCmdList["GSHELP"] = function(msg)
    PrintHelp()
end
