local _, ns = ...

------------------------------------------------------------
-- Class colors
------------------------------------------------------------
local CLASS_COLORS = {
    WARRIOR     = { 0.78, 0.61, 0.43 },
    PALADIN     = { 0.96, 0.55, 0.73 },
    HUNTER      = { 0.67, 0.83, 0.45 },
    ROGUE       = { 1.00, 0.96, 0.41 },
    PRIEST      = { 1.00, 1.00, 1.00 },
    DEATHKNIGHT = { 0.77, 0.12, 0.23 },
    SHAMAN      = { 0.00, 0.44, 0.87 },
    MAGE        = { 0.25, 0.78, 0.92 },
    WARLOCK     = { 0.53, 0.53, 0.93 },
    MONK        = { 0.00, 1.00, 0.60 },
    DRUID       = { 1.00, 0.49, 0.04 },
    DEMONHUNTER = { 0.64, 0.19, 0.79 },
    EVOKER      = { 0.20, 0.58, 0.50 },
}

local TIER_COUNT_COLORS = {
    [0] = { 1.0, 0.3, 0.3 },
    [1] = { 1.0, 0.6, 0.3 },
    [2] = { 1.0, 1.0, 0.3 },
    [3] = { 0.7, 1.0, 0.3 },
    [4] = { 0.3, 1.0, 0.3 },
    [5] = { 0.3, 1.0, 0.8 },
}

-- Raid buff definitions
local RAID_BUFFS = {
    { file = "MAGE",        buff = "Arcane Intellect",  desc = "5% Intellect" },
    { file = "WARRIOR",     buff = "Battle Shout",      desc = "5% Attack Power" },
    { file = "PRIEST",      buff = "Power Word: Fort",  desc = "5% Stamina" },
    { file = "MONK",        buff = "Mystic Touch",      desc = "5% Phys Dmg Taken" },
    { file = "DEMONHUNTER", buff = "Chaos Brand",       desc = "5% Magic Dmg Taken" },
    { file = "EVOKER",      buff = "Blessing of the Bronze", desc = "Movement/Haste" },
}

-- Role prefixes
local ROLE_PREFIX = { TANK = "|cff8888ffT|r", HEALER = "|cff00ff00H|r", DAMAGER = "|cffff4444D|r" }

------------------------------------------------------------
-- Raid Inspect Window
------------------------------------------------------------
local RAID_WIDTH = 400
local RAID_HEIGHT = 500
local ROW_HEIGHT = 20

local raidFrame = CreateFrame("Frame", "YippYappRaidFrame", UIParent, "BackdropTemplate")
raidFrame:SetSize(RAID_WIDTH, RAID_HEIGHT)
raidFrame:SetPoint("CENTER", 200, 0)
raidFrame:SetMovable(true)
raidFrame:EnableMouse(true)
raidFrame:RegisterForDrag("LeftButton")
raidFrame:SetScript("OnDragStart", raidFrame.StartMoving)
raidFrame:SetScript("OnDragStop", raidFrame.StopMovingOrSizing)
raidFrame:SetClampedToScreen(true)
raidFrame:SetFrameStrata("HIGH")
raidFrame:SetBackdrop({
    bgFile = "Interface\\Buttons\\WHITE8x8",
    edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
    edgeSize = 16,
    insets = { left = 4, right = 4, top = 4, bottom = 4 },
})
raidFrame:SetBackdropColor(0.08, 0.08, 0.08, 0.95)
raidFrame:SetBackdropBorderColor(0.6, 0.6, 0.6, 1)
ns.SmoothFrame(raidFrame)
raidFrame:Hide()
ns.RaidFrame = raidFrame

-- ESC to close
raidFrame:SetScript("OnShow", function()
    if raidFrame.inAppMode then return end
    tinsert(UISpecialFrames, "YippYappRaidFrame")
end)
raidFrame:SetScript("OnHide", function()
    for i = #UISpecialFrames, 1, -1 do
        if UISpecialFrames[i] == "YippYappRaidFrame" then
            table.remove(UISpecialFrames, i)
            break
        end
    end
end)

-- Close button
local close = CreateFrame("Button", nil, raidFrame, "UIPanelCloseButton")
close:SetPoint("TOPRIGHT", -2, -2)

function ns:SetRaidAppMode(enabled, contentWidth, contentHeight)
    if enabled then
        raidFrame:SetMovable(false)
        raidFrame:EnableMouse(false)
        raidFrame:SetBackdrop(nil)
        close:Hide()
        local dw, dh = ns:GetAppFrameSize()
        raidFrame:SetSize(contentWidth or dw, contentHeight or (dh - 34))
    else
        raidFrame:SetMovable(true)
        raidFrame:EnableMouse(true)
        raidFrame:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
            edgeSize = 16,
            insets = { left = 4, right = 4, top = 4, bottom = 4 },
        })
        raidFrame:SetBackdropColor(0.08, 0.08, 0.08, 0.95)
        raidFrame:SetBackdropBorderColor(0.6, 0.6, 0.6, 1)
        close:Show()
        raidFrame:SetSize(RAID_WIDTH, RAID_HEIGHT)
    end
end

------------------------------------------------------------
-- Tab system: Overview (default) | Tier Tracker
------------------------------------------------------------
local currentTab = "overview"
local rescanBtn  -- forward declare

local overviewContainer = CreateFrame("Frame", nil, raidFrame)
overviewContainer:SetPoint("TOPLEFT", 0, -34)
overviewContainer:SetPoint("BOTTOMRIGHT", 0, 0)

local tierContainer = CreateFrame("Frame", nil, raidFrame)
tierContainer:SetPoint("TOPLEFT", 0, -34)
tierContainer:SetPoint("BOTTOMRIGHT", 0, 0)
tierContainer:Hide()

local perfContainer = CreateFrame("Frame", nil, raidFrame)
perfContainer:SetPoint("TOPLEFT", 0, -34)
perfContainer:SetPoint("BOTTOMRIGHT", 0, 0)
perfContainer:Hide()

-- Reuse overviewContainer for groups content (merged)
local groupsContainer = overviewContainer

local overviewTab = ns.CreateUnderlineTab(raidFrame, "Overview",    { 1.0, 1.0, 0.3 })
local perfTab     = ns.CreateUnderlineTab(raidFrame, "Performance", { 1.0, 0.53, 0.0 })
local tierTab     = ns.CreateUnderlineTab(raidFrame, "Tier Tracker",{ 0.3, 1.0, 0.3 })

overviewTab:SetSize(110, 24)
overviewTab:SetPoint("TOPLEFT", 8, -8)
perfTab:SetSize(110, 24)
perfTab:SetPoint("TOPLEFT", 122, -8)
tierTab:SetSize(110, 24)
tierTab:SetPoint("TOPLEFT", 236, -8)

local function UpdateTabStyles()
    ns.SetTabInactive(overviewTab)
    ns.SetTabInactive(perfTab)
    ns.SetTabInactive(tierTab)
    overviewContainer:Hide()
    perfContainer:Hide()
    tierContainer:Hide()
    if rescanBtn then rescanBtn:Hide() end

    if currentTab == "overview" then
        ns.SetTabActive(overviewTab)
        overviewContainer:Show()
    elseif currentTab == "performance" then
        ns.SetTabActive(perfTab)
        perfContainer:Show()
    elseif currentTab == "tier" then
        ns.SetTabActive(tierTab)
        tierContainer:Show()
        if rescanBtn then rescanBtn:Show() end
    end
end

overviewTab:SetScript("OnClick", function()
    currentTab = "overview"
    UpdateTabStyles()
    ns:RefreshRaidOverview()
end)
perfTab:SetScript("OnClick", function()
    currentTab = "performance"
    UpdateTabStyles()
    ns:RefreshRaidPerformance()
end)
tierTab:SetScript("OnClick", function()
    currentTab = "tier"
    UpdateTabStyles()
end)

-- Apply initial tab styling
UpdateTabStyles()

function ns.SetRaidTab(tabName)
    currentTab = tabName
    UpdateTabStyles()
    if tabName == "overview" then
        ns:RefreshRaidOverview()
    elseif tabName == "performance" then
        ns:RefreshRaidPerformance()
    end
end

------------------------------------------------------------
-- Tier Tracker tab content
------------------------------------------------------------
rescanBtn = CreateFrame("Button", nil, raidFrame, "BackdropTemplate")
rescanBtn:SetSize(60, 18)
rescanBtn:SetPoint("TOPRIGHT", -36, -34)
rescanBtn:SetBackdrop({
    bgFile = "Interface\\Buttons\\WHITE8x8",
    edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
    edgeSize = 10,
    insets = { left = 2, right = 2, top = 2, bottom = 2 },
})
rescanBtn:SetBackdropColor(0.15, 0.15, 0.15, 1)
rescanBtn:SetBackdropBorderColor(0.5, 0.5, 0.5, 0.8)
rescanBtn:SetFrameLevel(raidFrame:GetFrameLevel() + 10)
rescanBtn:Hide()

local rescanText = rescanBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
rescanText:SetAllPoints()
rescanText:SetText("|cff00ff00Rescan|r")

rescanBtn:SetScript("OnClick", function() ns:ScanRaid() end)
rescanBtn:SetScript("OnEnter", function(self)
    self:SetBackdropBorderColor(0.3, 1.0, 0.3, 0.8)
    GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
    GameTooltip:SetText("Rescan raid for tier pieces")
    GameTooltip:Show()
end)
rescanBtn:SetScript("OnLeave", function(self)
    self:SetBackdropBorderColor(0.5, 0.5, 0.5, 0.8)
    GameTooltip_Hide()
end)

local summaryText = tierContainer:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
summaryText:SetPoint("TOPLEFT", 14, -6)
summaryText:SetWidth(RAID_WIDTH - 110)
summaryText:SetJustifyH("LEFT")

local divider = tierContainer:CreateTexture(nil, "ARTWORK")
divider:SetHeight(1)
divider:SetPoint("TOPLEFT", 12, -22)
divider:SetPoint("TOPRIGHT", -12, -22)
divider:SetColorTexture(0.5, 0.5, 0.5, 0.6)

local headerName = tierContainer:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
headerName:SetPoint("TOPLEFT", 14, -26)
headerName:SetText("|cffaaaaaaPlayer|r")

local headerTier = tierContainer:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
headerTier:SetPoint("TOPLEFT", 138, -26)
headerTier:SetText("|cffaaaaaaTier|r")

local headerPieces = tierContainer:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
headerPieces:SetPoint("TOPLEFT", 212, -26)
headerPieces:SetText("|cffaaaaaaPieces|r")

local scrollFrame = CreateFrame("ScrollFrame", nil, tierContainer, "UIPanelScrollFrameTemplate")
scrollFrame:SetPoint("TOPLEFT", 12, -40)
scrollFrame:SetPoint("BOTTOMRIGHT", -28, 12)

local scrollContent = CreateFrame("Frame", nil, scrollFrame)
scrollContent:SetSize(RAID_WIDTH - 44, 1)
scrollFrame:SetScrollChild(scrollContent)

ns.RaidRows = {}

------------------------------------------------------------
-- Helper: gather raid roster data
------------------------------------------------------------
local function GetRaidRoster()
    local roster = {}
    local n = GetNumGroupMembers()
    if n == 0 then return roster end

    if IsInRaid() then
        for i = 1, n do
            local name, rank, subgroup, level, class, fileName, zone, online, isDead, role, isML, combatRole = GetRaidRosterInfo(i)
            if name then
                local assignedRole = UnitGroupRolesAssigned("raid" .. i) or "NONE"
                table.insert(roster, {
                    name = name:match("^([^%-]+)") or name,
                    fullName = name,
                    class = fileName,
                    group = subgroup or 1,
                    role = assignedRole,
                    online = online,
                    index = i,
                })
            end
        end
    else
        -- Party mode
        local units = { "player" }
        for i = 1, n - 1 do
            table.insert(units, "party" .. i)
        end
        for i, unit in ipairs(units) do
            if UnitExists(unit) then
                local name = UnitName(unit)
                local _, fileName = UnitClass(unit)
                local assignedRole = UnitGroupRolesAssigned(unit) or "NONE"
                if name then
                    table.insert(roster, {
                        name = name,
                        fullName = name,
                        class = fileName,
                        group = 1,
                        role = assignedRole,
                        online = UnitIsConnected(unit),
                        index = i,
                    })
                end
            end
        end
    end
    return roster
end

------------------------------------------------------------
-- Shared section layout helpers (consistent across all tabs)
------------------------------------------------------------
local SECTION_PAD = 14
local SECTION_LINE_H = 20
local SECTION_HEADER_COLOR = "ffaaaaaa"

local function MakeSectionHeader(parent, y, text)
    local fs = parent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    fs:SetPoint("TOPLEFT", SECTION_PAD, y)
    fs:SetText("|c" .. SECTION_HEADER_COLOR .. text .. "|r")
    return fs, y - SECTION_LINE_H
end

local function MakeDivider(parent, y)
    local div = parent:CreateTexture(nil, "ARTWORK")
    div:SetHeight(1)
    div:SetPoint("TOPLEFT", SECTION_PAD - 2, y + 2)
    div:SetPoint("RIGHT", parent, "RIGHT", -SECTION_PAD, 0)
    div:SetColorTexture(0.3, 0.3, 0.3, 0.5)
    return y - 8
end

------------------------------------------------------------
-- Consumable / buff checking
-- Note: In 12.0+, aura name fields on other players are secret/tainted strings.
-- We use spellId-based detection instead of name matching.
------------------------------------------------------------

-- Known spell IDs for consumables (Midnight Season 1)
local FLASK_SPELL_IDS = {
    -- Flasks / Phials
    [432021] = true, -- Flask of Alchemical Chaos
    [431972] = true, -- Flask of Tempered Aggression
    [431973] = true, -- Flask of Tempered Swiftness
    [431974] = true, -- Flask of Tempered Mastery
    [431975] = true, -- Flask of Tempered Versatility
    [432180] = true, -- Flask of the Shattered Sun
}

local FOOD_SPELL_IDS = {
    [104280] = true, -- Well Fed (generic)
}

local RUNE_SPELL_IDS = {
    [453247] = true, -- Void-Touched Augment Rune
    [393438] = true, -- Crystallized Augment Rune
}

local function HasBuff(unit, checkFunc)
    for i = 1, 40 do
        local data = C_UnitAuras and C_UnitAuras.GetBuffDataByIndex(unit, i)
        if not data then break end
        local ok, result = pcall(checkFunc, data)
        if ok and result then return true end
    end
    return false
end

local function HasFlask(unit)
    return HasBuff(unit, function(data)
        -- Check by spell ID first (reliable, works with tainted names)
        if data.spellId and FLASK_SPELL_IDS[data.spellId] then return true end
        -- Fallback: long duration buff (>30min) from self that isn't harmful
        if data.duration and data.duration >= 1800 and data.isFromPlayerOrPlayerPet and not data.isHarmful then
            -- Try name match with pcall (works for own buffs, tainted for others)
            local ok, name = pcall(tostring, data.name)
            if ok and name then
                if name:find("Flask") or name:find("Phial") or name:find("Tempered") then return true end
            end
        end
        return false
    end)
end

local function HasFood(unit)
    return HasBuff(unit, function(data)
        if data.spellId and FOOD_SPELL_IDS[data.spellId] then return true end
        -- Well Fed has many spell IDs across expansions; try name as fallback
        local ok, name = pcall(tostring, data.name)
        if ok and name and name:find("Well Fed") then return true end
        return false
    end)
end

local function HasAugmentRune(unit)
    return HasBuff(unit, function(data)
        if data.spellId and RUNE_SPELL_IDS[data.spellId] then return true end
        local ok, name = pcall(tostring, data.name)
        if ok and name and (name:find("Augment") or name:find("Crystallized")) then return true end
        return false
    end)
end

local function GetUnitForRosterIndex(index, isRaid)
    if isRaid then
        return "raid" .. index
    else
        if index == 1 then return "player" end
        return "party" .. (index - 1)
    end
end

-- Helper: format a player name with class color
local function ColorName(p)
    local cc = CLASS_COLORS[p.class] or { 0.7, 0.7, 0.7 }
    local hex = string.format("ff%02x%02x%02x", cc[1] * 255, cc[2] * 255, cc[3] * 255)
    return "|c" .. hex .. p.name .. "|r"
end

-- Helper: find a player by name in roster
local function FindPlayer(roster, name)
    for _, p in ipairs(roster) do
        if p.name == name then return p end
    end
    return nil
end

------------------------------------------------------------
-- Overview tab content
------------------------------------------------------------
local ovPool = {}
local ovPoolIdx = 0

local function OvAcquire(parent, font)
    ovPoolIdx = ovPoolIdx + 1
    local fs = ovPool[ovPoolIdx]
    if not fs then
        fs = parent:CreateFontString(nil, "OVERLAY", font or "GameFontNormal")
        ovPool[ovPoolIdx] = fs
    else
        fs:SetFontObject(font or "GameFontNormal")
        fs:SetTextColor(1, 1, 1)
    end
    fs:ClearAllPoints()
    fs:SetWordWrap(false)
    fs:SetWidth(raidFrame:GetWidth() - 28)
    fs:SetJustifyH("LEFT")
    fs:Show()
    return fs
end

function ns:RefreshRaidOverview()
    for i = 1, ovPoolIdx do ovPool[i]:Hide() end
    ovPoolIdx = 0

    local roster = GetRaidRoster()

    if #roster == 0 then
        local fs = OvAcquire(overviewContainer, "GameFontNormal")
        fs:SetPoint("TOPLEFT", SECTION_PAD, -8)
        fs:SetText("|cff888888Not in a group|r")
        return
    end

    local tanks, healers, dps = 0, 0, 0
    local classCounts = {}
    for _, p in ipairs(roster) do
        if p.role == "TANK" then tanks = tanks + 1
        elseif p.role == "HEALER" then healers = healers + 1
        else dps = dps + 1 end
        classCounts[p.class] = (classCounts[p.class] or 0) + 1
    end

    -- ── Two-column layout ──
    -- Left: Groups + Splits + Swaps
    -- Right: Composition + Buffs + Classes + PI
    local fw = raidFrame:GetWidth() - 28
    local rightX = math.floor(fw * 0.55) + SECTION_PAD  -- right column starts at ~55%
    local rightW = fw - rightX + SECTION_PAD

    -- ── RIGHT COLUMN ──
    local ry = -8

    -- Composition (large, prominent)
    local compFs = OvAcquire(overviewContainer, "GameFontNormalLarge")
    compFs:SetPoint("TOPLEFT", rightX, ry)
    compFs:SetWidth(rightW)
    compFs:SetText("|cff8888ff" .. tanks .. " Tank|r   " ..
        "|cff00ff00" .. healers .. " Heal|r   " ..
        "|cffff4444" .. dps .. " DPS|r")
    ns.ApplyTextShadow(compFs)
    ry = ry - 22

    local totalFs = OvAcquire(overviewContainer, "GameFontNormalSmall")
    totalFs:SetPoint("TOPLEFT", rightX, ry)
    totalFs:SetTextColor(unpack(ns.COLORS.TEXT_TERTIARY))
    totalFs:SetText(#roster .. " players in group")
    ry = ry - 20

    -- Raid Buffs (one per line, cleaner)
    local buffHdr = OvAcquire(overviewContainer, "GameFontNormal")
    buffHdr:SetPoint("TOPLEFT", rightX, ry)
    buffHdr:SetTextColor(unpack(ns.COLORS.TEXT_HEADER))
    buffHdr:SetText("Raid Buffs")
    ns.ApplyTextShadow(buffHdr)
    ry = ry - SECTION_LINE_H

    for _, rb in ipairs(RAID_BUFFS) do
        local have = classCounts[rb.file] and classCounts[rb.file] > 0
        local bfs = OvAcquire(overviewContainer, "GameFontNormal")
        bfs:SetPoint("TOPLEFT", rightX + 4, ry)
        bfs:SetWidth(rightW)
        if have then
            bfs:SetText("|cff00ff00+|r  " .. rb.buff)
            bfs:SetTextColor(unpack(ns.COLORS.TEXT_PRIMARY))
        else
            bfs:SetText("|cffff0000-|r  |cffff4444" .. rb.buff .. "|r")
        end
        ry = ry - 17
    end

    ry = ry - 6

    -- Consumables
    local isRaid = IsInRaid()
    local flaskCount, foodCount, runeCount = 0, 0, 0
    local missingFlask, missingFood = {}, {}

    for _, p in ipairs(roster) do
        local unit = GetUnitForRosterIndex(p.index, isRaid)
        if UnitExists(unit) and UnitIsConnected(unit) then
            if HasFlask(unit) then
                flaskCount = flaskCount + 1
            else
                table.insert(missingFlask, p)
            end
            if HasFood(unit) then
                foodCount = foodCount + 1
            else
                table.insert(missingFood, p)
            end
            if HasAugmentRune(unit) then
                runeCount = runeCount + 1
            end
        end
    end

    local consHdr = OvAcquire(overviewContainer, "GameFontNormal")
    consHdr:SetPoint("TOPLEFT", rightX, ry)
    consHdr:SetWidth(rightW)
    consHdr:SetTextColor(unpack(ns.COLORS.TEXT_HEADER))
    consHdr:SetText("Consumables")
    ns.ApplyTextShadow(consHdr)
    ry = ry - SECTION_LINE_H

    local total = #roster
    local flaskColor = flaskCount == total and "ff00ff00" or (flaskCount >= total * 0.8 and "ffffff00" or "ffff4444")
    local foodColor = foodCount == total and "ff00ff00" or (foodCount >= total * 0.8 and "ffffff00" or "ffff4444")
    local runeColor = runeCount == total and "ff00ff00" or "ff888888"

    -- Flask line
    local flaskFs = OvAcquire(overviewContainer, "GameFontNormal")
    flaskFs:SetPoint("TOPLEFT", rightX + 4, ry)
    flaskFs:SetWidth(rightW)
    local flaskNames = ""
    if #missingFlask > 0 and #missingFlask <= 4 then
        local names = {}
        for _, p in ipairs(missingFlask) do table.insert(names, ColorName(p)) end
        flaskNames = "  |cff555555-|r " .. table.concat(names, " ")
    elseif #missingFlask > 4 then
        flaskNames = "  |cffff4444" .. #missingFlask .. " missing|r"
    end
    flaskFs:SetText("|c" .. flaskColor .. flaskCount .. "/" .. total .. "|r  Flask" .. flaskNames)
    ry = ry - 17

    -- Food line
    local foodFs = OvAcquire(overviewContainer, "GameFontNormal")
    foodFs:SetPoint("TOPLEFT", rightX + 4, ry)
    foodFs:SetWidth(rightW)
    local foodNames = ""
    if #missingFood > 0 and #missingFood <= 4 then
        local names = {}
        for _, p in ipairs(missingFood) do table.insert(names, ColorName(p)) end
        foodNames = "  |cff555555-|r " .. table.concat(names, " ")
    elseif #missingFood > 4 then
        foodNames = "  |cffff4444" .. #missingFood .. " missing|r"
    end
    foodFs:SetText("|c" .. foodColor .. foodCount .. "/" .. total .. "|r  Food" .. foodNames)
    ry = ry - 17

    -- Rune line
    local runeFs = OvAcquire(overviewContainer, "GameFontNormal")
    runeFs:SetPoint("TOPLEFT", rightX + 4, ry)
    runeFs:SetWidth(rightW)
    runeFs:SetText("|c" .. runeColor .. runeCount .. "/" .. total .. "|r  Augment Rune")

    -- ── LEFT COLUMN: groups/splits (starts from top, full left side) ──
    ns:RefreshRaidGroups(roster, -8)
end

------------------------------------------------------------
-- Groups tab content
------------------------------------------------------------
local grpPool = {}
local grpPoolIdx = 0
local swapFramePool = {}  -- separate pool for swap badge/button frames
local swapFramePoolIdx = 0

local function AcquireSwapFrame(parent, template)
    swapFramePoolIdx = swapFramePoolIdx + 1
    local f = swapFramePool[swapFramePoolIdx]
    if not f then
        f = CreateFrame(template or "Frame", nil, parent, "BackdropTemplate")
        swapFramePool[swapFramePoolIdx] = f
    else
        f:SetParent(parent)
    end
    f:ClearAllPoints()
    -- Clear old children font strings by hiding them
    for _, region in pairs({ f:GetRegions() }) do
        if region:IsObjectType("FontString") then
            region:SetText("")
        end
    end
    f:Show()
    return f
end

local function GrpAcquire(parent, font)
    grpPoolIdx = grpPoolIdx + 1
    local fs = grpPool[grpPoolIdx]
    if not fs then
        fs = parent:CreateFontString(nil, "OVERLAY", font or "GameFontNormal")
        grpPool[grpPoolIdx] = fs
    else
        fs:SetFontObject(font or "GameFontNormal")
        fs:SetTextColor(1, 1, 1)
    end
    fs:ClearAllPoints()
    fs:SetWordWrap(false)
    fs:Show()
    return fs
end

-- PI target assignments (session only, keyed by priest name)
ns.PIAssignments = ns.PIAssignments or {}

-- PI picker popup
local piPicker = CreateFrame("Frame", "YippYappPIPicker", UIParent, "BackdropTemplate")
piPicker:SetSize(180, 200)
piPicker:SetFrameStrata("DIALOG")
piPicker:SetClampedToScreen(true)
piPicker:SetBackdrop({
    bgFile   = "Interface\\Buttons\\WHITE8x8",
    edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
    edgeSize = 12,
    insets   = { left = 3, right = 3, top = 3, bottom = 3 },
})
piPicker:SetBackdropColor(0.06, 0.06, 0.06, 0.97)
piPicker:SetBackdropBorderColor(0.5, 0.5, 0.5, 0.8)
piPicker:EnableMouse(true)
piPicker:Hide()

piPicker:SetScript("OnShow", function()
    tinsert(UISpecialFrames, "YippYappPIPicker")
end)
piPicker:SetScript("OnHide", function()
    for i = #UISpecialFrames, 1, -1 do
        if UISpecialFrames[i] == "YippYappPIPicker" then
            table.remove(UISpecialFrames, i)
            break
        end
    end
end)

local piPickerTitle = piPicker:CreateFontString(nil, "OVERLAY", "GameFontNormal")
piPickerTitle:SetPoint("TOPLEFT", 10, -8)

local piPickerScroll = CreateFrame("ScrollFrame", nil, piPicker, "UIPanelScrollFrameTemplate")
piPickerScroll:SetPoint("TOPLEFT", 6, -28)
piPickerScroll:SetPoint("BOTTOMRIGHT", -24, 6)

local piPickerContent = CreateFrame("Frame", nil, piPickerScroll)
piPickerContent:SetSize(148, 1)
piPickerScroll:SetScrollChild(piPickerContent)

local piPickerRows = {}
local piPickerRowsUsed = 0
local piPickerPriestName = nil
local piPickerDivider = nil

local function AcquirePIPickerRow()
    piPickerRowsUsed = piPickerRowsUsed + 1
    local row = piPickerRows[piPickerRowsUsed]
    if not row then
        row = CreateFrame("Button", nil, piPickerContent)
        row:SetSize(148, 20)
        row._fs = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        row._fs:SetPoint("LEFT", 6, 0)
        row._hl = row:CreateTexture(nil, "HIGHLIGHT")
        row._hl:SetAllPoints()
        row._hl:SetColorTexture(1, 1, 1, 0.05)
        piPickerRows[piPickerRowsUsed] = row
    end
    row:ClearAllPoints()
    -- Hide the optional active-row background
    if row._bg then row._bg:Hide() end
    row:Show()
    return row
end

local function ShowPIPicker(anchorBtn, priestName, roster)
    piPickerPriestName = priestName

    -- Hide old rows
    for i = 1, piPickerRowsUsed do piPickerRows[i]:Hide() end
    piPickerRowsUsed = 0

    piPickerTitle:SetText("|cffaaaaaaPick PI target|r")

    -- "Clear" option at top
    local yOff = 0
    local clearBtn = AcquirePIPickerRow()
    clearBtn:SetPoint("TOPLEFT", 0, -yOff)
    clearBtn._fs:SetText("|cff888888Clear assignment|r")
    clearBtn:SetScript("OnClick", function()
        ns.PIAssignments[priestName] = nil
        piPicker:Hide()
        ns:RefreshRaidGroups()
    end)
    yOff = yOff + 22

    -- Divider (create once, reuse)
    if not piPickerDivider then
        piPickerDivider = piPickerContent:CreateTexture(nil, "ARTWORK")
        piPickerDivider:SetHeight(1)
        piPickerDivider:SetPoint("RIGHT", piPickerContent, "RIGHT", 0, 0)
        piPickerDivider:SetColorTexture(0.3, 0.3, 0.3, 0.5)
    end
    piPickerDivider:ClearAllPoints()
    piPickerDivider:SetPoint("TOPLEFT", 0, -yOff)
    piPickerDivider:SetPoint("RIGHT", piPickerContent, "RIGHT", 0, 0)
    piPickerDivider:Show()
    yOff = yOff + 4

    -- List all DPS and tanks
    local targets = {}
    for _, p in ipairs(roster) do
        if p.role == "DAMAGER" or p.role == "TANK" then
            table.insert(targets, p)
        end
    end
    table.sort(targets, function(a, b) return a.name < b.name end)

    local currentTarget = ns.PIAssignments[priestName]

    for _, p in ipairs(targets) do
        local row = AcquirePIPickerRow()
        row:SetPoint("TOPLEFT", 0, -yOff)

        local roleTag = ROLE_PREFIX[p.role] or ""
        local nameText = roleTag .. " " .. ColorName(p) .. " |cff555555G" .. p.group .. "|r"
        if p.name == currentTarget then
            nameText = nameText .. "  |cff00ff00<|r"
        end
        row._fs:SetText(nameText)

        if p.name == currentTarget then
            if not row._bg then
                row._bg = row:CreateTexture(nil, "BACKGROUND")
                row._bg:SetAllPoints()
                row._bg:SetColorTexture(0, 1, 0, 0.06)
            end
            row._bg:Show()
        end

        local targetName = p.name
        row:SetScript("OnClick", function()
            ns.PIAssignments[priestName] = targetName
            piPicker:Hide()
            ns:RefreshRaidGroups()
        end)

        yOff = yOff + 20
    end

    piPickerContent:SetHeight(math.max(yOff, 1))

    -- Size the popup to fit (max 300px tall)
    local popupH = math.min(yOff + 36, 300)
    piPicker:SetHeight(popupH)

    -- Anchor to the button
    piPicker:ClearAllPoints()
    piPicker:SetPoint("TOPLEFT", anchorBtn, "TOPRIGHT", 4, 0)
    piPicker:Show()
end

-- PI assignment buttons (created on demand)
local piBtnPool = {}
local piBtnPoolIdx = 0

local function AcquirePIButton(parent)
    piBtnPoolIdx = piBtnPoolIdx + 1
    local btn = piBtnPool[piBtnPoolIdx]
    if not btn then
        btn = CreateFrame("Button", nil, parent, "BackdropTemplate")
        btn:SetSize(200, 20)
        btn:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
            edgeSize = 8,
            insets = { left = 2, right = 2, top = 2, bottom = 2 },
        })
        btn.fs = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        btn.fs:SetPoint("LEFT", 8, 0)
        btn.targetFs = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        btn.targetFs:SetPoint("RIGHT", -8, 0)
        btn:SetScript("OnEnter", function(self)
            self:SetBackdropBorderColor(0.6, 0.6, 0.6, 0.8)
        end)
        btn:SetScript("OnLeave", function(self)
            self:SetBackdropBorderColor(0.3, 0.3, 0.3, 0.5)
        end)
        piBtnPool[piBtnPoolIdx] = btn
    end
    btn:SetBackdropColor(0.1, 0.1, 0.1, 0.8)
    btn:SetBackdropBorderColor(0.3, 0.3, 0.3, 0.5)
    btn:ClearAllPoints()
    btn:Show()
    return btn
end

-- Helper: find which split a group belongs to (true = A/odd, false = B/even)
local function IsOddGroup(g) return g % 2 == 1 end

function ns:RefreshRaidGroups(roster, startY)
    for i = 1, grpPoolIdx do grpPool[i]:Hide() end
    grpPoolIdx = 0
    for i = 1, piBtnPoolIdx do piBtnPool[i]:Hide() end
    piBtnPoolIdx = 0
    for i = 1, swapFramePoolIdx do swapFramePool[i]:Hide() end
    swapFramePoolIdx = 0

    -- If called standalone (from SetRaidTab), fetch roster ourselves
    if not roster then
        roster = GetRaidRoster()
    end
    local baseY = startY or -8

    if #roster == 0 then
        return
    end

    -- Sort roster into groups
    local groups = {}
    for i = 1, 8 do groups[i] = {} end
    for _, p in ipairs(roster) do
        local g = p.group or 1
        if g >= 1 and g <= 8 then
            table.insert(groups[g], p)
        end
    end

    local maxGroup = 0
    for g = 1, 8 do
        if #groups[g] > 0 then maxGroup = g end
    end

    -- ── Group display (3 columns, 2 rows: top=1,3,5  bottom=2,4,6) — left 55% of the frame ──
    local fw = math.floor((raidFrame:GetWidth() - 28) * 0.55)
    local colCount = 3
    local colW = math.floor(fw / colCount)
    local memberLineH = 18
    local groupBlockH = 22 + 5 * memberLineH + 10

    local grpStartY = MakeDivider(groupsContainer, baseY)

    local rowOrder = {
        { 1, 3, 5 },  -- top row
        { 2, 4, 6 },  -- bottom row
    }

    for row, groupNums in ipairs(rowOrder) do
        for col, g in ipairs(groupNums) do
            if g <= maxGroup and #groups[g] > 0 then
                local x = SECTION_PAD + (col - 1) * colW
                local cellY = grpStartY - (row - 1) * groupBlockH

                local hdr = GrpAcquire(groupsContainer, "GameFontNormal")
                hdr:SetPoint("TOPLEFT", groupsContainer, "TOPLEFT", x, cellY)
                hdr:SetText("|c" .. SECTION_HEADER_COLOR .. "Group " .. g .. "|r")

                for j, p in ipairs(groups[g]) do
                    local mfs = GrpAcquire(groupsContainer, "GameFontNormal")
                    mfs:SetPoint("TOPLEFT", groupsContainer, "TOPLEFT", x + 8, cellY - 4 - j * memberLineH)
                    local roleTag = ROLE_PREFIX[p.role] or " "
                    mfs:SetText(roleTag .. " " .. ColorName(p))
                end
            end
        end
    end

    -- ── Analysis section (always reserve 2 rows for groups 1-6) ──
    local analysisY = grpStartY - 2 * groupBlockH - 4

    analysisY = MakeDivider(groupsContainer, analysisY)

    -- Gather split data
    local splitA = { healers = {}, tanks = {}, dps = {}, all = {} }
    local splitB = { healers = {}, tanks = {}, dps = {}, all = {} }
    local allHealers = {}
    local allTanks = {}
    local allPriests = {}
    local allDPS = {}

    for g = 1, math.min(maxGroup, 6) do
        local split = IsOddGroup(g) and splitA or splitB
        for _, p in ipairs(groups[g]) do
            table.insert(split.all, p)
            if p.role == "HEALER" then
                table.insert(split.healers, p)
                table.insert(allHealers, p)
            elseif p.role == "TANK" then
                table.insert(split.tanks, p)
                table.insert(allTanks, p)
            else
                table.insert(split.dps, p)
                table.insert(allDPS, p)
            end
            if p.class == "PRIEST" then
                table.insert(allPriests, p)
            end
        end
    end

    -- ── Split Balance ──
    local totalA = #splitA.all
    local totalB = #splitB.all
    local countDiff = totalA - totalB

    local fs = GrpAcquire(groupsContainer, "GameFontNormal")
    fs:SetPoint("TOPLEFT", groupsContainer, "TOPLEFT", SECTION_PAD, analysisY)
    local balanceColor = math.abs(countDiff) >= 2 and "ffff4444" or "ff00ff00"
    fs:SetText("|c" .. SECTION_HEADER_COLOR .. "Split Balance|r   " ..
        string.format("|cffffffffA (1,3,5):|r %d |cff555555(%dT %dH %dD)|r   |cffffffffB (2,4,6):|r %d |cff555555(%dT %dH %dD)|r",
            totalA, #splitA.tanks, #splitA.healers, #splitA.dps,
            totalB, #splitB.tanks, #splitB.healers, #splitB.dps))
    analysisY = analysisY - SECTION_LINE_H

    -- ── Swap Suggestions ──
    -- Each suggestion: { p1 = player to move, p2 = swap with (or nil for just move), reason = short label }
    local suggestions = {}

    local healDiff = #splitA.healers - #splitB.healers

    -- Player count balance
    if countDiff >= 2 then
        local moveP = (healDiff >= 1 and #splitA.healers > 1) and splitA.healers[#splitA.healers] or splitA.dps[#splitA.dps]
        local swapWith = splitB.dps[1]
        if moveP then
            table.insert(suggestions, { p1 = moveP, p2 = swapWith, reason = "Balance size" })
        end
    elseif countDiff <= -2 then
        local moveP = (healDiff <= -1 and #splitB.healers > 1) and splitB.healers[#splitB.healers] or splitB.dps[#splitB.dps]
        local swapWith = splitA.dps[1]
        if moveP then
            table.insert(suggestions, { p1 = moveP, p2 = swapWith, reason = "Balance size" })
        end
    end

    -- Helper: get healer "spec" key (class for non-priests, class_role for priests)
    local function HealerSpecKey(p)
        -- Use class as spec proxy (we can't inspect spec, but class is enough
        -- to detect duplicate healer types like 2x Disc Priest)
        return p.class
    end

    -- Tank balance: want 1 per split
    if #splitA.tanks >= 2 and #splitB.tanks == 0 then
        local swapWith = splitB.dps[1] or splitB.all[1]
        if swapWith then
            table.insert(suggestions, { p1 = splitA.tanks[2], p2 = swapWith, reason = "Tank split" })
        end
    elseif #splitB.tanks >= 2 and #splitA.tanks == 0 then
        local swapWith = splitA.dps[1] or splitA.all[1]
        if swapWith then
            table.insert(suggestions, { p1 = splitB.tanks[2], p2 = swapWith, reason = "Tank split" })
        end
    end

    -- Healer balance: want equal count per split + no spec stacking
    if healDiff >= 2 then
        local moveH = nil
        local specCountA = {}
        for _, h in ipairs(splitA.healers) do specCountA[HealerSpecKey(h)] = (specCountA[HealerSpecKey(h)] or 0) + 1 end
        for _, h in ipairs(splitA.healers) do
            if specCountA[HealerSpecKey(h)] >= 2 then moveH = h break end
        end
        moveH = moveH or splitA.healers[#splitA.healers]
        if moveH then
            table.insert(suggestions, { p1 = moveH, p2 = splitB.dps[1], reason = "Healer balance" })
        end
    elseif healDiff <= -2 then
        local moveH = nil
        local specCountB = {}
        for _, h in ipairs(splitB.healers) do specCountB[HealerSpecKey(h)] = (specCountB[HealerSpecKey(h)] or 0) + 1 end
        for _, h in ipairs(splitB.healers) do
            if specCountB[HealerSpecKey(h)] >= 2 then moveH = h break end
        end
        moveH = moveH or splitB.healers[#splitB.healers]
        if moveH then
            table.insert(suggestions, { p1 = moveH, p2 = splitA.dps[1], reason = "Healer balance" })
        end
    end

    -- Healer spec stacking (only flag if it's actually fixable)
    -- With 5 healers and duplicates, some stacking is inevitable.
    -- Only suggest a swap if moving one would not leave a split with <2 healers.
    if math.abs(healDiff) < 2 then
        local specOnA, specOnB = {}, {}
        for _, h in ipairs(splitA.healers) do local k = HealerSpecKey(h); specOnA[k] = specOnA[k] or {}; table.insert(specOnA[k], h) end
        for _, h in ipairs(splitB.healers) do local k = HealerSpecKey(h); specOnB[k] = specOnB[k] or {}; table.insert(specOnB[k], h) end
        for spec, list in pairs(specOnA) do
            if #list >= 2 and not specOnB[spec] and #splitA.healers > 2 then
                table.insert(suggestions, { p1 = list[2], p2 = splitB.dps[1], reason = "Healer stacked" })
            end
        end
        for spec, list in pairs(specOnB) do
            if #list >= 2 and not specOnA[spec] and #splitB.healers > 2 then
                table.insert(suggestions, { p1 = list[2], p2 = splitA.dps[1], reason = "Healer stacked" })
            end
        end
    end

    -- DK Grip distribution: want at least 1 DK per split for Mass Grip
    local dkA, dkB = {}, {}
    for _, p in ipairs(splitA.all) do if p.class == "DEATHKNIGHT" then table.insert(dkA, p) end end
    for _, p in ipairs(splitB.all) do if p.class == "DEATHKNIGHT" then table.insert(dkB, p) end end
    if #dkA >= 2 and #dkB == 0 then
        local swapWith = splitB.dps[1]
        if swapWith then
            table.insert(suggestions, { p1 = dkA[2], p2 = swapWith, reason = "Split Grip" })
        end
    elseif #dkB >= 2 and #dkA == 0 then
        local swapWith = splitA.dps[1]
        if swapWith then
            table.insert(suggestions, { p1 = dkB[2], p2 = swapWith, reason = "Split Grip" })
        end
    end

    -- PI pairing: same split
    for _, priest in ipairs(allPriests) do
        local targetName = ns.PIAssignments[priest.name]
        if targetName then
            local target = FindPlayer(roster, targetName)
            if target and IsOddGroup(priest.group) ~= IsOddGroup(target.group) then
                table.insert(suggestions, { p1 = priest, p2 = target, reason = "PI pairing" })
            end
        end
    end

    -- Debuff class distribution
    local DEBUFF_CLASSES = {
        { file = "MONK",        short = "Mystic Touch" },
        { file = "DEMONHUNTER", short = "Chaos Brand" },
    }
    for _, dc in ipairs(DEBUFF_CLASSES) do
        local pA, pB = {}, {}
        for _, p in ipairs(splitA.all) do if p.class == dc.file then table.insert(pA, p) end end
        for _, p in ipairs(splitB.all) do if p.class == dc.file then table.insert(pB, p) end end
        -- Only suggest if one side has 2+ and the other has 0 (actionable swap)
        if #pA >= 2 and #pB == 0 then
            local swapWith = splitB.dps[1]
            if swapWith then
                table.insert(suggestions, { p1 = pA[2], p2 = swapWith, reason = dc.short })
            end
        elseif #pB >= 2 and #pA == 0 then
            local swapWith = splitA.dps[1]
            if swapWith then
                table.insert(suggestions, { p1 = pB[2], p2 = swapWith, reason = dc.short })
            end
        end
    end

    -- ── Left: Suggested Swaps (visual cards with Apply button) ──
    -- Helper: check if we can move raid members
    local function CanMoveRaidMembers()
        if InCombatLockdown() then return false end
        if not IsInRaid() then return false end
        local _, rank = GetRaidRosterInfo(1) -- check our rank
        -- Actually check the player's own rank
        for i = 1, GetNumGroupMembers() do
            local name, r = GetRaidRosterInfo(i)
            if name and UnitIsUnit("raid" .. i, "player") then
                return r >= 1 -- 1 = assistant, 2 = leader
            end
        end
        return false
    end

    local canMove = CanMoveRaidMembers()

    -- Filter out suggestions without a swap partner
    local actionable = {}
    for _, s in ipairs(suggestions) do
        if s.p1 and s.p2 then
            table.insert(actionable, s)
        end
    end

    if #actionable > 0 then
        -- Header + "Swap All" button
        local sHdr = GrpAcquire(groupsContainer, "GameFontNormal")
        sHdr:SetPoint("TOPLEFT", groupsContainer, "TOPLEFT", SECTION_PAD, analysisY)
        sHdr:SetText("|cffffffaaSuggested Swaps|r")
        ns.ApplyTextShadow(sHdr)

        -- "Swap All" button (top-right of suggestions header)
        if canMove then
            local swapAllBtn = AcquireSwapFrame(groupsContainer, "Button")
            swapAllBtn:SetSize(60, 18)
            swapAllBtn:SetPoint("TOPLEFT", groupsContainer, "TOPLEFT", SECTION_PAD + fw - 70, analysisY + 1)
            swapAllBtn:SetBackdrop({
                bgFile = "Interface\\Buttons\\WHITE8x8",
                edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
                edgeSize = 8,
                insets = { left = 2, right = 2, top = 2, bottom = 2 },
            })
            swapAllBtn:SetBackdropColor(0.0, 0.3, 0.1, 1)
            swapAllBtn:SetBackdropBorderColor(0.0, 0.8, 0.2, 0.6)
            ns.SmoothFrame(swapAllBtn)

            if not swapAllBtn._fs then
                swapAllBtn._fs = swapAllBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
                swapAllBtn._fs:SetPoint("CENTER")
            end
            swapAllBtn._fs:SetText("|cffffffffSwap All|r")

            local swapData = {}
            for _, s in ipairs(actionable) do
                table.insert(swapData, { idx1 = s.p1.index, idx2 = s.p2.index })
            end

            swapAllBtn:SetScript("OnClick", function()
                if InCombatLockdown() then
                    print("|cff00ff00YippYapp|r: Can't swap during combat")
                    return
                end
                for _, sd in ipairs(swapData) do
                    SwapRaidSubgroup(sd.idx1, sd.idx2)
                end
                C_Timer.After(0.5, function() ns:RefreshRaidOverview() end)
            end)
        end

        analysisY = analysisY - SECTION_LINE_H

        for _, s in ipairs(actionable) do
            local rowY = analysisY
            local x = SECTION_PAD + 8

            -- Name badge 1
            local badge1 = AcquireSwapFrame(groupsContainer)
            badge1:SetHeight(20)
            badge1:SetPoint("TOPLEFT", groupsContainer, "TOPLEFT", x, rowY)
            badge1:SetBackdrop({
                bgFile = "Interface\\Buttons\\WHITE8x8",
                edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
                edgeSize = 8, insets = { left = 2, right = 2, top = 2, bottom = 2 },
            })
            badge1:SetBackdropColor(0.08, 0.08, 0.08, 0.8)
            local cc1 = CLASS_COLORS[s.p1.class] or { 0.5, 0.5, 0.5 }
            badge1:SetBackdropBorderColor(cc1[1] * 0.5, cc1[2] * 0.5, cc1[3] * 0.5, 0.7)
            if not badge1._fs then
                badge1._fs = badge1:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
                badge1._fs:SetPoint("LEFT", 6, 0)
            end
            badge1._fs:SetText(ColorName(s.p1) .. " |cff444444G" .. s.p1.group .. "|r")
            badge1:SetWidth(badge1._fs:GetStringWidth() + 16)

            -- Arrow
            local arrowFs = GrpAcquire(groupsContainer, "GameFontNormal")
            arrowFs:SetPoint("LEFT", badge1, "RIGHT", 6, 0)
            arrowFs:SetTextColor(unpack(ns.COLORS.TEXT_TERTIARY))
            arrowFs:SetText("<>")

            -- Name badge 2
            local badge2 = AcquireSwapFrame(groupsContainer)
            badge2:SetHeight(20)
            badge2:SetPoint("LEFT", arrowFs, "RIGHT", 6, 0)
            badge2:SetBackdrop({
                bgFile = "Interface\\Buttons\\WHITE8x8",
                edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
                edgeSize = 8, insets = { left = 2, right = 2, top = 2, bottom = 2 },
            })
            badge2:SetBackdropColor(0.08, 0.08, 0.08, 0.8)
            local cc2 = CLASS_COLORS[s.p2.class] or { 0.5, 0.5, 0.5 }
            badge2:SetBackdropBorderColor(cc2[1] * 0.5, cc2[2] * 0.5, cc2[3] * 0.5, 0.7)
            if not badge2._fs then
                badge2._fs = badge2:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
                badge2._fs:SetPoint("LEFT", 6, 0)
            end
            badge2._fs:SetText(ColorName(s.p2) .. " |cff444444G" .. s.p2.group .. "|r")
            badge2:SetWidth(badge2._fs:GetStringWidth() + 16)

            analysisY = analysisY - 22

            -- Reason subtitle
            local reasonRow = GrpAcquire(groupsContainer, "GameFontNormalSmall")
            reasonRow:SetPoint("TOPLEFT", groupsContainer, "TOPLEFT", SECTION_PAD + 12, analysisY)
            reasonRow:SetTextColor(unpack(ns.COLORS.TEXT_TERTIARY))
            reasonRow:SetText(s.reason)
            analysisY = analysisY - 14
        end
    else
        local okFs = GrpAcquire(groupsContainer, "GameFontNormal")
        okFs:SetPoint("TOPLEFT", groupsContainer, "TOPLEFT", SECTION_PAD, analysisY)
        okFs:SetText("|cff00ff00Splits look balanced|r")
        analysisY = analysisY - SECTION_LINE_H
    end

    -- ── Right: PI Assignments (independent Y, anchored to right column) ──
    local fullW = raidFrame:GetWidth() - 28
    local piX = math.floor(fullW * 0.55) + SECTION_PAD
    local piW = fullW - piX + SECTION_PAD
    local piY = grpStartY - 2 * groupBlockH - 4
    piY = piY - 8  -- align with divider

    local piHdr = GrpAcquire(groupsContainer, "GameFontNormal")
    piHdr:SetPoint("TOPLEFT", groupsContainer, "TOPLEFT", piX, piY)
    piHdr:SetText("|c" .. SECTION_HEADER_COLOR .. "PI Targets|r" ..
        (#allPriests == 0 and "  |cffff4444No priests|r" or ""))
    ns.ApplyTextShadow(piHdr)
    piY = piY - SECTION_LINE_H + 2

    -- Build DPS target list for PI selection
    local dpsTargets = {}
    for _, p in ipairs(roster) do
        if p.role == "DAMAGER" or p.role == "TANK" then
            table.insert(dpsTargets, p)
        end
    end
    table.sort(dpsTargets, function(a, b) return a.name < b.name end)

    for _, priest in ipairs(allPriests) do
        local btn = AcquirePIButton(groupsContainer)
        btn:SetWidth(math.min(piW - 8, 300))
        btn:SetPoint("TOPLEFT", groupsContainer, "TOPLEFT", piX + 4, piY)

        btn.fs:SetText(ColorName(priest) .. " |cff555555G" .. priest.group .. "|r")

        local currentTarget = ns.PIAssignments[priest.name]
        if currentTarget then
            local tp = FindPlayer(roster, currentTarget)
            if tp then
                btn.targetFs:SetText("-> " .. ColorName(tp))
            else
                btn.targetFs:SetText("-> |cff888888" .. currentTarget .. "|r")
            end
        else
            btn.targetFs:SetText("|cff555555Click to assign|r")
        end

        local priestName = priest.name
        btn:SetScript("OnClick", function(self)
            if piPicker:IsShown() and piPickerPriestName == priestName then
                piPicker:Hide()
            else
                ShowPIPicker(self, priestName, roster)
            end
        end)

        piY = piY - 24
    end
end

------------------------------------------------------------
-- Performance tab (C_DamageMeter API)
------------------------------------------------------------
local perfPool = {}
local perfPoolIdx = 0

local function PerfAcquire(font)
    perfPoolIdx = perfPoolIdx + 1
    local fs = perfPool[perfPoolIdx]
    if not fs then
        fs = perfContainer:CreateFontString(nil, "OVERLAY", font or "GameFontNormal")
        perfPool[perfPoolIdx] = fs
    else
        fs:SetFontObject(font or "GameFontNormal")
        fs:SetTextColor(1, 1, 1)
    end
    fs:ClearAllPoints()
    fs:SetWordWrap(false)
    fs:SetWidth(raidFrame:GetWidth() - 28)
    fs:SetJustifyH("LEFT")
    fs:Show()
    return fs
end

local function FormatDPS(amount)
    if amount >= 1000000 then
        return string.format("%.1fM", amount / 1000000)
    elseif amount >= 1000 then
        return string.format("%.1fK", amount / 1000)
    end
    return tostring(math.floor(amount))
end

local function GetCombatData(sessionType, meterType)
    if not C_DamageMeter or not C_DamageMeter.GetCombatSessionFromType then
        return nil
    end
    local ok, session = pcall(C_DamageMeter.GetCombatSessionFromType, sessionType, meterType)
    if ok and session then
        return session
    end
    return nil
end

function ns:RefreshRaidPerformance()
    for i = 1, perfPoolIdx do perfPool[i]:Hide() end
    perfPoolIdx = 0

    local y = -8

    -- Try Overall first, fallback to Current
    local SESSION_OVERALL = 0
    local SESSION_CURRENT = 1
    local TYPE_DPS = 1
    local TYPE_DAMAGE_TAKEN = 7
    local TYPE_AVOIDABLE = 8
    local TYPE_DEATHS = 9
    local TYPE_INTERRUPTS = 5

    local dpsSession = GetCombatData(SESSION_CURRENT, TYPE_DPS)
        or GetCombatData(SESSION_OVERALL, TYPE_DPS)

    if not dpsSession or not dpsSession.combatSources or #dpsSession.combatSources == 0 then
        local fs = PerfAcquire("GameFontNormal")
        fs:SetPoint("TOPLEFT", SECTION_PAD, y)
        fs:SetText("|cff888888No combat data available|r")
        local fs2 = PerfAcquire("GameFontNormalSmall")
        fs2:SetPoint("TOPLEFT", SECTION_PAD, y - SECTION_LINE_H)
        fs2:SetTextColor(unpack(ns.COLORS.TEXT_SECONDARY))
        fs2:SetText("Data appears after combat (requires Blizzard Damage Meter enabled)")
        return
    end

    local fw = raidFrame:GetWidth() - 28
    local leftW = math.floor(fw * 0.5)
    local rightX = leftW + SECTION_PAD

    -- ── LEFT: DPS Rankings + Split DPS ──
    local hdr = PerfAcquire("GameFontNormal")
    hdr:SetPoint("TOPLEFT", SECTION_PAD, y)
    ns.ApplyTextShadow(hdr)

    local duration = dpsSession.durationSeconds
    local durStr = duration and string.format(" |cff555555(%dm %ds)|r", math.floor(duration / 60), duration % 60) or ""
    hdr:SetText("|c" .. SECTION_HEADER_COLOR .. "DPS|r" .. durStr)
    y = y - SECTION_LINE_H

    -- Sort by DPS descending
    local dpsList = {}
    for _, src in ipairs(dpsSession.combatSources) do
        if src.name and src.classFilename then
            table.insert(dpsList, src)
        end
    end
    table.sort(dpsList, function(a, b) return a.amountPerSecond > b.amountPerSecond end)

    -- Calculate split DPS from ALL players (not just top 10)
    local roster = GetRaidRoster()
    local splitDPS = { A = 0, B = 0 }
    local splitCount = { A = 0, B = 0 }
    local playerGroups = {}
    for _, p in ipairs(roster) do
        playerGroups[p.name] = p.group
    end

    for _, src in ipairs(dpsList) do
        local name = src.name:match("^([^%-]+)") or src.name
        local grp = playerGroups[name]
        if grp then
            local split = (grp % 2 == 1) and "A" or "B"
            splitDPS[split] = splitDPS[split] + src.amountPerSecond
            splitCount[split] = splitCount[split] + 1
        end
    end

    -- Show top 10 DPS
    local maxDPS = dpsList[1] and dpsList[1].amountPerSecond or 1
    for i = 1, math.min(#dpsList, 10) do
        local src = dpsList[i]
        local cc = CLASS_COLORS[src.classFilename] or { 0.7, 0.7, 0.7 }
        local hex = string.format("ff%02x%02x%02x", cc[1] * 255, cc[2] * 255, cc[3] * 255)

        local fs = PerfAcquire("GameFontNormalSmall")
        fs:SetPoint("TOPLEFT", SECTION_PAD + 4, y)
        fs:SetWidth(leftW - 8)
        local name = src.name:match("^([^%-]+)") or src.name
        fs:SetText(string.format("|c%s%s|r  %s", hex, name, FormatDPS(src.amountPerSecond)))
        y = y - 14
    end

    if #dpsList > 10 then
        local mfs = PerfAcquire("GameFontNormalSmall")
        mfs:SetPoint("TOPLEFT", SECTION_PAD + 4, y)
        mfs:SetTextColor(unpack(ns.COLORS.TEXT_TERTIARY))
        mfs:SetText("+" .. (#dpsList - 10) .. " more")
        y = y - 14
    end

    -- Split DPS comparison (average per player)
    y = y - 4
    local avgA = splitCount.A > 0 and (splitDPS.A / splitCount.A) or 0
    local avgB = splitCount.B > 0 and (splitDPS.B / splitCount.B) or 0
    local avgDiff = math.abs(avgA - avgB)
    local avgTotal = avgA + avgB
    local imbalanced = avgTotal > 0 and (avgDiff / avgTotal) > 0.15

    local splitFs = PerfAcquire("GameFontNormal")
    splitFs:SetPoint("TOPLEFT", SECTION_PAD, y)
    ns.ApplyTextShadow(splitFs)
    local splitColor = imbalanced and "|cffff4444" or "|cff00ff00"
    splitFs:SetText("|c" .. SECTION_HEADER_COLOR .. "Avg DPS per player|r")
    y = y - SECTION_LINE_H

    local splitDetail = PerfAcquire("GameFontNormal")
    splitDetail:SetPoint("TOPLEFT", SECTION_PAD + 4, y)
    splitDetail:SetText(
        "A (1,3,5): " .. FormatDPS(avgA) .. " |cff555555(" .. splitCount.A .. " players)|r   " ..
        "B (2,4,6): " .. FormatDPS(avgB) .. " |cff555555(" .. splitCount.B .. " players)|r" ..
        (imbalanced and ("  " .. splitColor .. math.floor(avgDiff / avgTotal * 100) .. "% diff|r") or ""))

    -- ── RIGHT COLUMN: Avoidable damage, Deaths, Interrupts ──
    local ry = -8

    -- Avoidable Damage
    local avoidSession = GetCombatData(SESSION_CURRENT, TYPE_AVOIDABLE)
        or GetCombatData(SESSION_OVERALL, TYPE_AVOIDABLE)
    if avoidSession and avoidSession.combatSources and #avoidSession.combatSources > 0 then
        local ahdr = PerfAcquire("GameFontNormal")
        ahdr:SetPoint("TOPLEFT", rightX, ry)
        ahdr:SetText("|c" .. SECTION_HEADER_COLOR .. "Avoidable Damage Taken|r")
        ns.ApplyTextShadow(ahdr)
        ry = ry - SECTION_LINE_H

        local avoidList = {}
        for _, src in ipairs(avoidSession.combatSources) do
            if src.name and src.totalAmount > 0 then
                table.insert(avoidList, src)
            end
        end
        table.sort(avoidList, function(a, b) return a.totalAmount > b.totalAmount end)

        for i = 1, math.min(#avoidList, 5) do
            local src = avoidList[i]
            local cc = CLASS_COLORS[src.classFilename] or { 0.7, 0.7, 0.7 }
            local hex = string.format("ff%02x%02x%02x", cc[1] * 255, cc[2] * 255, cc[3] * 255)
            local name = src.name:match("^([^%-]+)") or src.name
            local afs = PerfAcquire("GameFontNormalSmall")
            afs:SetPoint("TOPLEFT", rightX + 4, ry)
            afs:SetWidth(fw - leftW - 8)
            afs:SetText(string.format("|cffff4444%s|r  |c%s%s|r", FormatDPS(src.totalAmount), hex, name))
            ry = ry - 14
        end
        ry = ry - 4
    end

    -- Deaths
    local deathSession = GetCombatData(SESSION_CURRENT, TYPE_DEATHS)
        or GetCombatData(SESSION_OVERALL, TYPE_DEATHS)
    if deathSession and deathSession.combatSources and #deathSession.combatSources > 0 then
        local dhdr = PerfAcquire("GameFontNormal")
        dhdr:SetPoint("TOPLEFT", rightX, ry)
        dhdr:SetText("|c" .. SECTION_HEADER_COLOR .. "Deaths|r")
        ns.ApplyTextShadow(dhdr)
        ry = ry - SECTION_LINE_H

        local deathList = {}
        for _, src in ipairs(deathSession.combatSources) do
            if src.name and src.totalAmount > 0 then
                table.insert(deathList, src)
            end
        end
        table.sort(deathList, function(a, b) return a.totalAmount > b.totalAmount end)

        for i = 1, math.min(#deathList, 5) do
            local src = deathList[i]
            local cc = CLASS_COLORS[src.classFilename] or { 0.7, 0.7, 0.7 }
            local hex = string.format("ff%02x%02x%02x", cc[1] * 255, cc[2] * 255, cc[3] * 255)
            local name = src.name:match("^([^%-]+)") or src.name
            local dfs = PerfAcquire("GameFontNormalSmall")
            dfs:SetPoint("TOPLEFT", rightX + 4, ry)
            dfs:SetWidth(fw - leftW - 8)
            dfs:SetText(string.format("|cffff4444%dx|r  |c%s%s|r", src.totalAmount, hex, name))
            ry = ry - 14
        end
        ry = ry - 4
    end

    -- Interrupts
    local intSession = GetCombatData(SESSION_CURRENT, TYPE_INTERRUPTS)
        or GetCombatData(SESSION_OVERALL, TYPE_INTERRUPTS)
    if intSession and intSession.combatSources and #intSession.combatSources > 0 then
        local ihdr = PerfAcquire("GameFontNormal")
        ihdr:SetPoint("TOPLEFT", rightX, ry)
        ihdr:SetText("|c" .. SECTION_HEADER_COLOR .. "Interrupts|r")
        ns.ApplyTextShadow(ihdr)
        ry = ry - SECTION_LINE_H

        local intList = {}
        for _, src in ipairs(intSession.combatSources) do
            if src.name and src.totalAmount > 0 then
                table.insert(intList, src)
            end
        end
        table.sort(intList, function(a, b) return a.totalAmount > b.totalAmount end)

        for i = 1, math.min(#intList, 5) do
            local src = intList[i]
            local cc = CLASS_COLORS[src.classFilename] or { 0.7, 0.7, 0.7 }
            local hex = string.format("ff%02x%02x%02x", cc[1] * 255, cc[2] * 255, cc[3] * 255)
            local name = src.name:match("^([^%-]+)") or src.name
            local ifs = PerfAcquire("GameFontNormalSmall")
            ifs:SetPoint("TOPLEFT", rightX + 4, ry)
            ifs:SetWidth(fw - leftW - 8)
            ifs:SetText(string.format("|cff00aaff%d|r  |c%s%s|r", src.totalAmount, hex, name))
            ry = ry - 14
        end
    end
end

------------------------------------------------------------
-- Refresh tier display
------------------------------------------------------------
local tierRowPool = {}
local tierRowPoolIdx = 0

local function AcquireTierRow()
    tierRowPoolIdx = tierRowPoolIdx + 1
    local row = tierRowPool[tierRowPoolIdx]
    if not row then
        row = CreateFrame("Frame", nil, scrollContent)
        row:SetSize(RAID_WIDTH - 44, ROW_HEIGHT)
        row._bg = row:CreateTexture(nil, "BACKGROUND")
        row._bg:SetAllPoints()
        row._nameStr = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        row._nameStr:SetPoint("LEFT", 2, 0)
        row._nameStr:SetWidth(120)
        row._nameStr:SetJustifyH("LEFT")
        row._nameStr:SetWordWrap(false)
        row._tierStr = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        row._tierStr:SetPoint("LEFT", 126, 0)
        row._tierStr:SetWidth(70)
        row._tierStr:SetJustifyH("CENTER")
        row._piecesStr = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        row._piecesStr:SetPoint("LEFT", row, "LEFT", 200, 0)
        row._piecesStr:SetWidth(140)
        row._piecesStr:SetJustifyH("LEFT")
        row._piecesStr:SetWordWrap(false)
        row._piecesStr:SetTextColor(0.6, 0.6, 0.6)
        tierRowPool[tierRowPoolIdx] = row
    end
    row:ClearAllPoints()
    row:Show()
    return row
end

function ns:RefreshRaidDisplay()
    for i = 1, tierRowPoolIdx do tierRowPool[i]:Hide() end
    tierRowPoolIdx = 0
    -- Also hide legacy rows if any
    for _, row in ipairs(ns.RaidRows) do row:Hide() end
    wipe(ns.RaidRows)

    local sorted = ns:GetSortedTierData()
    local summary = ns:GetTierSummary()

    summaryText:SetText(string.format(
        "%d players scanned | 2-set: |cff00ff00%d|r | 4-set: |cff00ff00%d|r | No tier: |cffff4444%d|r",
        summary.total, summary.with2set, summary.with4set, summary.counts[0] or 0
    ))

    local yOffset = 0
    for i, data in ipairs(sorted) do
        local row = AcquireTierRow()
        row:SetPoint("TOPLEFT", 0, -yOffset)

        if i % 2 == 0 then
            row._bg:SetColorTexture(1, 1, 1, 0.03)
            row._bg:Show()
        else
            row._bg:Hide()
        end

        local cc = CLASS_COLORS[data.class] or { 1, 1, 1 }
        row._nameStr:SetTextColor(cc[1], cc[2], cc[3])

        local displayName = data.name
        if data.tierCount >= 4 then
            displayName = displayName .. "  |cff888888PASS|r"
        end
        row._nameStr:SetText(displayName)

        local tc = TIER_COUNT_COLORS[data.tierCount] or { 1, 1, 1 }
        row._tierStr:SetTextColor(tc[1], tc[2], tc[3])

        local tierLabel = data.tierCount .. "/5"
        if data.tierCount >= 4 then
            tierLabel = tierLabel .. " (4-set)"
        elseif data.tierCount >= 2 then
            tierLabel = tierLabel .. " (2-set)"
        end
        row._tierStr:SetText(tierLabel)

        if data.tierCount >= 4 then
            row._piecesStr:SetText("max")
        elseif #data.tierSlots > 0 then
            row._piecesStr:SetText(table.concat(data.tierSlots, ", "))
        else
            row._piecesStr:SetText("--")
        end

        yOffset = yOffset + ROW_HEIGHT
    end

    scrollContent:SetHeight(math.max(yOffset, 1))
end

------------------------------------------------------------
-- Auto-refresh on roster changes
------------------------------------------------------------
local rosterEventFrame = CreateFrame("Frame")
rosterEventFrame:RegisterEvent("GROUP_ROSTER_UPDATE")
rosterEventFrame:SetScript("OnEvent", function()
    -- Only refresh if the raid frame is visible and on the overview tab
    if raidFrame:IsShown() and currentTab == "overview" then
        ns:RefreshRaidOverview()
    end
end)
