SFrames.Party = {}
local _A = SFrames.ActiveTheme

local PARTY_FRAME_WIDTH = 150
local PARTY_FRAME_HEIGHT = 35
local PARTY_VERTICAL_GAP = 30
local PARTY_HORIZONTAL_GAP = 8

local PARTY_UNIT_LOOKUP = { party1 = true, party2 = true, party3 = true, party4 = true }
local PARTYPET_UNIT_LOOKUP = { partypet1 = true, partypet2 = true, partypet3 = true, partypet4 = true }

local function GetIncomingHeals(unit)
    return SFrames:GetIncomingHeals(unit)
end

local function TryDropCursorOnUnit(unit)
    if not unit or not UnitExists(unit) then return false end
    if not CursorHasItem or not CursorHasItem() then return false end
    if not DropItemOnUnit then return false end

    local ok = pcall(DropItemOnUnit, unit)
    if not ok then
        return false
    end

    return not CursorHasItem()
end

local function Clamp(value, minValue, maxValue)
    if value < minValue then
        return minValue
    end
    if value > maxValue then
        return maxValue
    end
    return value
end

function SFrames.Party:GetMetrics()
    local db = SFramesDB or {}

    local width = tonumber(db.partyFrameWidth) or PARTY_FRAME_WIDTH
    width = Clamp(math.floor(width + 0.5), 120, 320)

    local height = tonumber(db.partyFrameHeight) or PARTY_FRAME_HEIGHT
    height = Clamp(math.floor(height + 0.5), 28, 80)

    local portraitWidth = tonumber(db.partyPortraitWidth) or math.min(33, height - 2)
    portraitWidth = Clamp(math.floor(portraitWidth + 0.5), 24, 64)
    if portraitWidth > width - 70 then
        portraitWidth = width - 70
    end

    local healthHeight = tonumber(db.partyHealthHeight) or math.floor((height - 3) * 0.7)
    healthHeight = Clamp(math.floor(healthHeight + 0.5), 10, height - 8)

    local powerHeight = tonumber(db.partyPowerHeight) or (height - healthHeight - 3)
    powerHeight = Clamp(math.floor(powerHeight + 0.5), 6, height - 6)

    if healthHeight + powerHeight + 3 > height then
        powerHeight = height - healthHeight - 3
        if powerHeight < 6 then
            powerHeight = 6
            healthHeight = height - powerHeight - 3
            if healthHeight < 10 then
                healthHeight = 10
                powerHeight = height - healthHeight - 3
            end
        end
    end

    local hgap = tonumber(db.partyHorizontalGap) or PARTY_HORIZONTAL_GAP
    hgap = Clamp(math.floor(hgap + 0.5), 0, 40)

    local vgap = tonumber(db.partyVerticalGap) or PARTY_VERTICAL_GAP
    vgap = Clamp(math.floor(vgap + 0.5), 0, 80)

    local nameFont = tonumber(db.partyNameFontSize) or 10
    nameFont = Clamp(math.floor(nameFont + 0.5), 8, 18)

    local valueFont = tonumber(db.partyValueFontSize) or 10
    valueFont = Clamp(math.floor(valueFont + 0.5), 8, 18)

    return {
        width = width,
        height = height,
        portraitWidth = portraitWidth,
        healthHeight = healthHeight,
        powerHeight = powerHeight,
        horizontalGap = hgap,
        verticalGap = vgap,
        nameFont = nameFont,
        valueFont = valueFont,
    }
end

function SFrames.Party:ApplyFrameStyle(frame, metrics)
    if not frame then return end

    frame:SetWidth(metrics.width)
    frame:SetHeight(metrics.height)

    if frame.pbg then
        frame.pbg:SetWidth(metrics.portraitWidth + 2)
        frame.pbg:SetHeight(metrics.height)
        frame.pbg:ClearAllPoints()
        frame.pbg:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
    end

    if frame.portrait then
        frame.portrait:SetWidth(metrics.portraitWidth)
        frame.portrait:SetHeight(metrics.height - 2)
        if frame.pbg then
            frame.portrait:ClearAllPoints()
            frame.portrait:SetPoint("CENTER", frame.pbg, "CENTER", 0, 0)
        end
    end

    if frame.health then
        frame.health:ClearAllPoints()
        frame.health:SetPoint("TOPLEFT", frame.pbg, "TOPRIGHT", 2, -1)
        frame.health:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -1, -1)
        frame.health:SetHeight(metrics.healthHeight)
    end

    if frame.healthBGFrame then
        frame.healthBGFrame:ClearAllPoints()
        frame.healthBGFrame:SetPoint("TOPLEFT", frame.health, "TOPLEFT", -1, 1)
        frame.healthBGFrame:SetPoint("BOTTOMRIGHT", frame.health, "BOTTOMRIGHT", 1, -1)
    end

    if frame.power then
        frame.power:ClearAllPoints()
        frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", 0, -1)
        frame.power:SetPoint("TOPRIGHT", frame.health, "BOTTOMRIGHT", 0, 0)
        frame.power:SetHeight(metrics.powerHeight)
    end

    if frame.powerBGFrame then
        frame.powerBGFrame:ClearAllPoints()
        frame.powerBGFrame:SetPoint("TOPLEFT", frame.power, "TOPLEFT", -1, 1)
        frame.powerBGFrame:SetPoint("BOTTOMRIGHT", frame.power, "BOTTOMRIGHT", 1, -1)
    end

    local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE"
    local fontPath = SFrames:GetFont()

    if frame.nameText then
        frame.nameText:SetFont(fontPath, metrics.nameFont, outline)
    end
    if frame.healthText then
        frame.healthText:SetFont(fontPath, metrics.valueFont, outline)
    end
end

function SFrames.Party:ApplyConfig()
    if not self.parent then return end

    local frameScale = tonumber(SFramesDB and SFramesDB.partyFrameScale) or 1
    frameScale = Clamp(frameScale, 0.7, 1.8)
    self.parent:SetScale(frameScale)

    local metrics = self:GetMetrics()
    self.metrics = metrics

    if self.frames then
        for i = 1, 4 do
            local item = self.frames[i]
            if item and item.frame then
                self:ApplyFrameStyle(item.frame, metrics)
            end
        end
    end

    self:ApplyLayout()
    if self.testing then
        for i = 1, 4 do
            if self.frames[i] and self.frames[i].frame then
                self.frames[i].frame:Show()
            end
        end
    else
        self:UpdateAll()
    end
end

function SFrames.Party:GetLayoutMode()
    if SFramesDB and SFramesDB.partyLayout == "horizontal" then
        return "horizontal"
    end
    return "vertical"
end

function SFrames.Party:SavePosition()
    if not (self.parent and SFramesDB) then return end
    if not SFramesDB.Positions then SFramesDB.Positions = {} end
    local point, _, relativePoint, xOfs, yOfs = self.parent:GetPoint()
    if point and relativePoint then
        SFramesDB.Positions["PartyFrame"] = {
            point = point,
            relativePoint = relativePoint,
            xOfs = xOfs or 0,
            yOfs = yOfs or 0,
        }
    end
end

function SFrames.Party:ApplyPosition()
    if not self.parent then return end
    self.parent:ClearAllPoints()
    local pos = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["PartyFrame"]
    if pos and pos.point and pos.relativePoint and type(pos.xOfs) == "number" and type(pos.yOfs) == "number" then
        self.parent:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs, pos.yOfs)
    else
        self.parent:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 15, -150)
    end
end

function SFrames.Party:ApplyLayout()
    if not (self.parent and self.frames) then return end

    local metrics = self.metrics or self:GetMetrics()
    self.metrics = metrics
    local mode = self:GetLayoutMode()
    for i = 1, 4 do
        local f = self.frames[i] and self.frames[i].frame
        if f then
            f:ClearAllPoints()
            if i == 1 then
                f:SetPoint("TOPLEFT", self.parent, "TOPLEFT", 0, 0)
            else
                local prev = self.frames[i - 1].frame
                if mode == "horizontal" then
                    f:SetPoint("TOPLEFT", prev, "TOPRIGHT", metrics.horizontalGap, 0)
                else
                    f:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -metrics.verticalGap)
                end
            end
        end
    end

    if mode == "horizontal" then
        self.parent:SetWidth((metrics.width * 4) + (metrics.horizontalGap * 3))
        self.parent:SetHeight(metrics.height)
    else
        self.parent:SetWidth(metrics.width)
        self.parent:SetHeight(metrics.height + ((metrics.height + metrics.verticalGap) * 3))
    end
end

function SFrames.Party:SetLayout(mode)
    if not SFramesDB then SFramesDB = {} end
    if mode ~= "horizontal" then
        mode = "vertical"
    end
    SFramesDB.partyLayout = mode
    self:ApplyLayout()
    if self.testing then
        for i = 1, 4 do
            self.frames[i].frame:Show()
        end
    else
        self:UpdateAll()
    end
end

function SFrames.Party:Initialize()
    self.frames = {}
    
    if not SFramesDB then SFramesDB = {} end
    if not SFramesDB.partyLayout then
        SFramesDB.partyLayout = "vertical"
    end

    local parent = CreateFrame("Frame", "SFramesPartyParent", UIParent)
    parent:SetWidth(PARTY_FRAME_WIDTH)
    parent:SetHeight(PARTY_FRAME_HEIGHT)
    local frameScale = (SFramesDB and type(SFramesDB.partyFrameScale) == "number") and SFramesDB.partyFrameScale or 1
    parent:SetScale(frameScale)
    self.parent = parent
    self:ApplyPosition()

    parent:SetMovable(true)
    
    for i = 1, 4 do
        local unit = "party" .. i
        local f = CreateFrame("Button", "SFramesPartyFrame"..i, parent)
        f:SetWidth(PARTY_FRAME_WIDTH)
        f:SetHeight(PARTY_FRAME_HEIGHT)
        
        f.id = i
        f:RegisterForClicks("LeftButtonUp", "RightButtonUp")
        f:RegisterForDrag("LeftButton")
        f:SetScript("OnDragStart", function()
            if IsAltKeyDown() or SFrames.isUnlocked then
                SFrames.Party.parent:StartMoving()
            end
        end)
        f:SetScript("OnDragStop", function()
            SFrames.Party.parent:StopMovingOrSizing()
            SFrames.Party:SavePosition()
        end)
        f:SetScript("OnClick", function()
            if arg1 == "LeftButton" then
                if TryDropCursorOnUnit(this.unit) then
                    return
                end
                if SpellIsTargeting and SpellIsTargeting() then
                    SpellTargetUnit(this.unit)
                    return
                end
                TargetUnit(this.unit)
                SFrames.Party:UpdateFrame(this.unit)
            elseif arg1 == "RightButton" then
                ToggleDropDownMenu(1, nil, getglobal("PartyMemberFrame"..this.id.."DropDown"), "cursor")
            end
        end)
        f:SetScript("OnReceiveDrag", function()
            if TryDropCursorOnUnit(this.unit) then
                return
            end
            if SpellIsTargeting and SpellIsTargeting() then
                SpellTargetUnit(this.unit)
            end
        end)
        f:SetScript("OnEnter", function()
            GameTooltip_SetDefaultAnchor(GameTooltip, this)
            GameTooltip:SetUnit(this.unit)
        end)
        f:SetScript("OnLeave", function()
            GameTooltip:Hide()
        end)
        f.unit = unit
        
        -- Portrait
        local pbg = CreateFrame("Frame", nil, f)
        pbg:SetWidth(35)
        pbg:SetHeight(35)
        pbg:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
        pbg:SetFrameLevel(f:GetFrameLevel() - 1)
        SFrames:CreateUnitBackdrop(pbg)
        f.pbg = pbg
        
        f.portrait = CreateFrame("PlayerModel", nil, f)
        f.portrait:SetWidth(33)
        f.portrait:SetHeight(33)
        f.portrait:SetPoint("CENTER", pbg, "CENTER", 0, 0)

        local offlineOverlay = CreateFrame("Frame", nil, f)
        offlineOverlay:SetFrameLevel((f.portrait:GetFrameLevel() or 0) + 3)
        offlineOverlay:SetAllPoints(pbg)
        local offlineIcon = SFrames:CreateIcon(offlineOverlay, "offline", 20)
        offlineIcon:SetPoint("CENTER", offlineOverlay, "CENTER", 0, 0)
        offlineIcon:SetVertexColor(0.7, 0.7, 0.7, 0.9)
        offlineIcon:Hide()
        f.offlineIcon = offlineIcon

        -- Health Bar
        f.health = SFrames:CreateStatusBar(f, "SFramesPartyFrame"..i.."Health")
        f.health:SetPoint("TOPLEFT", pbg, "TOPRIGHT", 2, -1)
        f.health:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 10)
        
        local hbg = CreateFrame("Frame", nil, f)
        hbg:SetPoint("TOPLEFT", f.health, "TOPLEFT", -1, 1)
        hbg:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 1, -1)
        hbg:SetFrameLevel(f:GetFrameLevel() - 1)
        SFrames:CreateUnitBackdrop(hbg)
        f.healthBGFrame = hbg
        
        f.health.bg = f.health:CreateTexture(nil, "BACKGROUND")
        f.health.bg:SetAllPoints()
        f.health.bg:SetTexture(SFrames:GetTexture())
        f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)

        -- Heal prediction overlay (incoming heals)
        f.health.healPredMine = f.health:CreateTexture(nil, "OVERLAY")
        f.health.healPredMine:SetTexture(SFrames:GetTexture())
        f.health.healPredMine:SetVertexColor(0.4, 1.0, 0.55, 0.78)
        f.health.healPredMine:Hide()

        f.health.healPredOther = f.health:CreateTexture(nil, "OVERLAY")
        f.health.healPredOther:SetTexture(SFrames:GetTexture())
        f.health.healPredOther:SetVertexColor(0.2, 0.9, 0.35, 0.5)
        f.health.healPredOther:Hide()
        
        -- Power Bar
        f.power = SFrames:CreateStatusBar(f, "SFramesPartyFrame"..i.."Power")
        f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1)
        f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1)
        f.power:SetMinMaxValues(0, 100)
        
        local powerbg = CreateFrame("Frame", nil, f)
        powerbg:SetPoint("TOPLEFT", f.power, "TOPLEFT", -1, 1)
        powerbg:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1)
        powerbg:SetFrameLevel(f:GetFrameLevel() - 1)
        SFrames:CreateUnitBackdrop(powerbg)
        f.powerBGFrame = powerbg
        
        f.power.bg = f.power:CreateTexture(nil, "BACKGROUND")
        f.power.bg:SetAllPoints()
        f.power.bg:SetTexture(SFrames:GetTexture())
        f.power.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
        
        -- Texts
        f.nameText = SFrames:CreateFontString(f.health, 10, "LEFT")
        f.nameText:SetPoint("LEFT", f.health, "LEFT", 4, 0)
        
        f.healthText = SFrames:CreateFontString(f.health, 10, "RIGHT")
        f.healthText:SetPoint("RIGHT", f.health, "RIGHT", -4, 0)
        
        f.nameText:SetShadowColor(0, 0, 0, 1)
        f.nameText:SetShadowOffset(1, -1)
        f.healthText:SetShadowColor(0, 0, 0, 1)
        f.healthText:SetShadowOffset(1, -1)
        
        -- Leader / Master Looter overlay (high frame level so icons aren't hidden by portrait)
        local roleOvr = CreateFrame("Frame", nil, f)
        roleOvr:SetFrameLevel((f:GetFrameLevel() or 0) + 4)
        roleOvr:SetAllPoints(f)

        -- Leader Icon
        f.leaderIcon = roleOvr:CreateTexture(nil, "OVERLAY")
        f.leaderIcon:SetWidth(14)
        f.leaderIcon:SetHeight(14)
        f.leaderIcon:SetPoint("TOPLEFT", pbg, "TOPLEFT", -4, 4)
        f.leaderIcon:SetTexture("Interface\\GroupFrame\\UI-Group-LeaderIcon")
        f.leaderIcon:Hide()
        
        -- Master Looter Icon
        f.masterIcon = roleOvr:CreateTexture(nil, "OVERLAY")
        f.masterIcon:SetWidth(12)
        f.masterIcon:SetHeight(12)
        f.masterIcon:SetPoint("TOPRIGHT", pbg, "TOPRIGHT", 4, 4)
        f.masterIcon:SetTexture("Interface\\GroupFrame\\UI-Group-MasterLooter")
        f.masterIcon:Hide()

        -- Raid Target Icon
        local raidIconSize = 18
        local raidIconOvr = CreateFrame("Frame", nil, f)
        raidIconOvr:SetFrameLevel((f:GetFrameLevel() or 0) + 5)
        raidIconOvr:SetWidth(raidIconSize)
        raidIconOvr:SetHeight(raidIconSize)
        raidIconOvr:SetPoint("CENTER", f.health, "TOP", 0, 0)
        f.raidIcon = raidIconOvr:CreateTexture(nil, "OVERLAY")
        f.raidIcon:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons")
        f.raidIcon:SetAllPoints(raidIconOvr)
        f.raidIcon:Hide()
        f.raidIconOverlay = raidIconOvr
        
        -- Pet Frame
        local pf = CreateFrame("Button", "SFramesPartyPetFrame"..i, f)
        pf:SetHeight(8)
        pf:SetPoint("BOTTOMLEFT", f.health, "TOPLEFT", 0, 2)
        pf:SetPoint("BOTTOMRIGHT", f.health, "TOPRIGHT", 0, 2)
        pf.unit = "partypet"..i
        
        SFrames:CreateUnitBackdrop(pf)
        pf.health = SFrames:CreateStatusBar(pf, "SFramesPartyPetHealth"..i)
        pf.health:SetAllPoints()
        pf.health.bg = pf.health:CreateTexture(nil, "BACKGROUND")
        pf.health.bg:SetAllPoints()
        pf.health.bg:SetTexture(SFrames:GetTexture())
        pf.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)

        pf.nameText = SFrames:CreateFontString(pf.health, 8, "LEFT")
        pf.nameText:SetPoint("LEFT", pf.health, "LEFT", 2, 0)
        pf.nameText:SetShadowColor(0, 0, 0, 1)
        pf.nameText:SetShadowOffset(1, -1)
        
        pf:RegisterForClicks("LeftButtonUp", "RightButtonUp")
        pf:SetScript("OnClick", function() TargetUnit(this.unit) end)
        pf:SetScript("OnEnter", function()
            GameTooltip_SetDefaultAnchor(GameTooltip, this)
            GameTooltip:SetUnit(this.unit)
        end)
        pf:SetScript("OnLeave", function() GameTooltip:Hide() end)
        pf:Hide()
        f.petFrame = pf

        self.frames[i] = { frame = f, unit = unit, index = i }
        f.unit = unit
        
        self:CreateAuras(i)
        
        f:SetScript("OnShow", function()
            if not SFrames.Party.testing then
                SFrames.Party:UpdateFrame(this.unit)
            end
        end)
        
        f:Hide()
    end

    if not self._globalUpdateFrame then
        self._globalUpdateFrame = CreateFrame("Frame", nil, UIParent)
        self._globalUpdateFrame:SetScript("OnUpdate", function()
            if SFrames.Party.testing then return end
            local dt = arg1
            local frames = SFrames.Party.frames
            if not frames then return end
            for i = 1, 4 do
                local entry = frames[i]
                if entry then
                    local f = entry.frame
                    if f:IsVisible() and f.unit and f.unit ~= "" then
                        f.rangeTimer = f.rangeTimer + dt
                        if f.rangeTimer >= 0.5 then
                            if UnitExists(f.unit) and not CheckInteractDistance(f.unit, 4) then
                                f:SetAlpha(0.5)
                            else
                                f:SetAlpha(1.0)
                            end
                            f.rangeTimer = 0
                        end

                        f.auraScanTimer = f.auraScanTimer + dt
                        if f.auraScanTimer >= 0.5 then
                            SFrames.Party:UpdateAuras(f.unit)
                            f.auraScanTimer = 0
                        end

                        f.healPredTimer = f.healPredTimer + dt
                        if f.healPredTimer >= 0.2 then
                            SFrames.Party:UpdateHealPrediction(f.unit)
                            f.healPredTimer = 0
                        end

                        f.tickAuraTimer = f.tickAuraTimer + dt
                        if f.tickAuraTimer >= 0.5 then
                            SFrames.Party:TickAuras(f.unit)
                            f.tickAuraTimer = 0
                        end
                    end
                end
            end
        end)
    end

    self:ApplyConfig()
    
    SFrames:RegisterEvent("PARTY_MEMBERS_CHANGED", function() self:UpdateAll() end)
    SFrames:RegisterEvent("RAID_ROSTER_UPDATE", function() self:UpdateAll() end)
    SFrames:RegisterEvent("UNIT_HEALTH", function()
        if PARTY_UNIT_LOOKUP[arg1] then self:UpdateHealth(arg1)
        elseif PARTYPET_UNIT_LOOKUP[arg1] then self:UpdatePet(arg1) end
    end)
    SFrames:RegisterEvent("UNIT_MAXHEALTH", function()
        if PARTY_UNIT_LOOKUP[arg1] then self:UpdateHealth(arg1)
        elseif PARTYPET_UNIT_LOOKUP[arg1] then self:UpdatePet(arg1) end
    end)
    SFrames:RegisterEvent("UNIT_MANA", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end)
    SFrames:RegisterEvent("UNIT_MAXMANA", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end)
    SFrames:RegisterEvent("UNIT_RAGE", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end)
    SFrames:RegisterEvent("UNIT_MAXRAGE", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end)
    SFrames:RegisterEvent("UNIT_ENERGY", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end)
    SFrames:RegisterEvent("UNIT_MAXENERGY", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end)
    SFrames:RegisterEvent("UNIT_FOCUS", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end)
    SFrames:RegisterEvent("UNIT_MAXFOCUS", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePower(arg1) end end)
    SFrames:RegisterEvent("UNIT_DISPLAYPOWER", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePowerType(arg1) end end)
    SFrames:RegisterEvent("UNIT_PORTRAIT_UPDATE", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePortrait(arg1) end end)
    SFrames:RegisterEvent("UNIT_AURA", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdateAuras(arg1) end end)
    SFrames:RegisterEvent("UNIT_LEVEL", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdateFrame(arg1) end end)
    SFrames:RegisterEvent("PARTY_LEADER_CHANGED", function() self:UpdateAll() end)
    SFrames:RegisterEvent("PARTY_LOOT_METHOD_CHANGED", function() self:UpdateAll() end)
    SFrames:RegisterEvent("UNIT_PET", function() if PARTY_UNIT_LOOKUP[arg1] then self:UpdatePet("partypet" .. string.sub(arg1, 6)) end end)
    SFrames:RegisterEvent("RAID_TARGET_UPDATE", function() self:UpdateRaidIcons() end)
    
    -- Bulletproof Party load syncer (watches for count changes for 10 seconds after reload)
    local syncWatcher = CreateFrame("Frame")
    local syncTimer = 0
    local lastCount = -1
    syncWatcher:SetScript("OnUpdate", function()
        syncTimer = syncTimer + arg1
        local count = GetNumPartyMembers()
        if count ~= lastCount then
            lastCount = count
            SFrames.Party:UpdateAll()
        end
        if syncTimer > 10.0 then
            this:SetScript("OnUpdate", nil)
        end
    end)
    
    self:UpdateAll()

    if SFrames.Movers and SFrames.Movers.RegisterMover and self.parent then
        SFrames.Movers:RegisterMover("PartyFrame", self.parent, "小队",
            "TOPLEFT", "UIParent", "TOPLEFT", 15, -150)
    end
end

function SFrames.Party:CreateAuras(index)
    local f = self.frames[index].frame
    f.buffs = {}
    f.debuffs = {}
    local size = 20
    local spacing = 2
    
    -- Party Buffs
    for i = 1, 4 do
        local b = CreateFrame("Button", "SFramesParty"..index.."Buff"..i, f)
        b:SetWidth(size)
        b:SetHeight(size)
        SFrames:CreateUnitBackdrop(b)
        
        b.icon = b:CreateTexture(nil, "ARTWORK")
        b.icon:SetAllPoints()
        b.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
        
        b.cdText = SFrames:CreateFontString(b, 9, "CENTER")
        b.cdText:SetPoint("BOTTOM", b, "BOTTOM", 0, 1)
        b.cdText:SetTextColor(1, 0.82, 0)
        b.cdText:SetShadowColor(0, 0, 0, 1)
        b.cdText:SetShadowOffset(1, -1)
        
        b:SetScript("OnEnter", function()
            GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT")
            GameTooltip:SetUnitBuff(f.unit, this:GetID())
        end)
        b:SetScript("OnLeave", function() GameTooltip:Hide() end)
        
        -- Anchored BELOW the frame on the left side
        if i == 1 then
            b:SetPoint("TOPLEFT", f, "BOTTOMLEFT", 0, -2)
        else
            b:SetPoint("LEFT", f.buffs[i-1], "RIGHT", spacing, 0)
        end
        
        b:Hide()
        f.buffs[i] = b
    end
    
    -- Debuffs (Starting right after Buffs to remain linear)
    for i = 1, 4 do
        local b = CreateFrame("Button", "SFramesParty"..index.."Debuff"..i, f)
        b:SetWidth(size)
        b:SetHeight(size)
        SFrames:CreateUnitBackdrop(b)
        
        b.icon = b:CreateTexture(nil, "ARTWORK")
        b.icon:SetAllPoints()
        b.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
        
        b.cdText = SFrames:CreateFontString(b, 9, "CENTER")
        b.cdText:SetPoint("BOTTOM", b, "BOTTOM", 0, 1)
        b.cdText:SetTextColor(1, 0.82, 0)
        b.cdText:SetShadowColor(0, 0, 0, 1)
        b.cdText:SetShadowOffset(1, -1)
        
        b:SetScript("OnEnter", function()
            GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT")
            GameTooltip:SetUnitDebuff(f.unit, this:GetID())
        end)
        b:SetScript("OnLeave", function() GameTooltip:Hide() end)
        
        if i == 1 then
            b:SetPoint("LEFT", f.buffs[4], "RIGHT", spacing * 4, 0)
        else
            b:SetPoint("LEFT", f.debuffs[i-1], "RIGHT", spacing, 0)
        end
        
        b:Hide()
        f.debuffs[i] = b
    end
    
    f.auraScanTimer = 0
    f.rangeTimer = 0
    f.healPredTimer = 0
    f.tickAuraTimer = 0
end

function SFrames.Party:UpdatePet(unit)
    local _, _, indexStr = string.find(unit, "(%d+)")
    local index = tonumber(indexStr)
    if not index or not self.frames[index] then return end
    local pf = self.frames[index].frame.petFrame
    if not pf then return end

    if UnitExists(unit) and UnitIsConnected("party"..index) then
        pf:Show()
        local name = UnitName(unit)
        if name == UNKNOWNOBJECT or name == "未知目标" or name == "Unknown" then
            name = "宠物"
        end
        pf.nameText:SetText(name)
        
        local hp = UnitHealth(unit)
        local maxHp = UnitHealthMax(unit)
        pf.health:SetMinMaxValues(0, maxHp)
        pf.health:SetValue(hp)
        pf.health:SetStatusBarColor(0.2, 0.8, 0.2)
    else
        pf:Hide()
    end
end


function SFrames.Party:TickAuras(unit)
    local data = self:GetFrameByUnit(unit)
    if not data then return end
    local f = data.frame
    local timeNow = GetTime()
    
    for i = 1, 4 do
        local b = f.buffs[i]
        if b:IsShown() and b.expirationTime then
            local timeLeft = b.expirationTime - timeNow
            if timeLeft > 0 and timeLeft < 3600 then
                b.cdText:SetText(SFrames:FormatTime(timeLeft))
            else
                b.cdText:SetText("")
            end
        end
        
        local db = f.debuffs[i]
        if db:IsShown() and db.expirationTime then
            local timeLeft = db.expirationTime - timeNow
            if timeLeft > 0 and timeLeft < 3600 then
                db.cdText:SetText(SFrames:FormatTime(timeLeft))
            else
                db.cdText:SetText("")
            end
        end
    end
end

function SFrames.Party:GetFrameByUnit(unit)
    for i = 1, 4 do
        if self.frames[i].unit == unit then
            return self.frames[i]
        end
    end
    return nil
end

function SFrames.Party:UpdateAll()
    if self.testing then return end
    
    local inRaid = GetNumRaidMembers() > 0
    local raidFramesEnabled = SFramesDB and SFramesDB.enableRaidFrames ~= false
    
    if inRaid and raidFramesEnabled then
        for i = 1, 4 do
            if self.frames[i] and self.frames[i].frame then
                self.frames[i].frame:Hide()
            end
        end
        return
    end
    if self.testing then return end
    local numParty = GetNumPartyMembers()
    for i = 1, 4 do
        local data = self.frames[i]
        local f = data.frame
        if i <= numParty then
            f:Show()
            self:UpdateFrame(data.unit)
        else
            f:Hide()
        end
    end
end

function SFrames.Party:UpdateFrame(unit)
    if self.testing then return end
    local data = self:GetFrameByUnit(unit)
    if not data then return end
    local f = data.frame
    
    f.portrait:SetUnit(unit)
    f.portrait:SetCamera(0)
    f.portrait:Hide()
    f.portrait:Show()
    
    local name = UnitName(unit) or ""
    local level = UnitLevel(unit)
    if level == -1 then level = "??" end
    
    local _, class = UnitClass(unit)
    
    if not UnitIsConnected(unit) then
        f.health:SetStatusBarColor(0.5, 0.5, 0.5)
        f.nameText:SetText(name)
        f.nameText:SetTextColor(0.5, 0.5, 0.5)
        if f.offlineIcon then f.offlineIcon:Show() end
    else
        if f.offlineIcon then f.offlineIcon:Hide() end
        if class and SFrames.Config.colors.class[class] then
            local color = SFrames.Config.colors.class[class]
            f.health:SetStatusBarColor(color.r, color.g, color.b)
            f.nameText:SetText(level .. " " .. name)
            f.nameText:SetTextColor(color.r, color.g, color.b)
        else
            f.health:SetStatusBarColor(0, 1, 0)
            f.nameText:SetText(level .. " " .. name)
            f.nameText:SetTextColor(1, 1, 1)
        end
    end
    
    -- Update Leader/Master Looter
    if GetPartyLeaderIndex() == data.index then
        f.leaderIcon:Show()
    else
        f.leaderIcon:Hide()
    end
    
    local method, partyIndex, raidIndex = GetLootMethod()
    if method == "master" and partyIndex == data.index then
        f.masterIcon:Show()
    else
        f.masterIcon:Hide()
    end
    
    self:UpdateHealth(unit)
    self:UpdatePowerType(unit)
    self:UpdatePower(unit)
    self:UpdateAuras(unit)
    self:UpdateRaidIcon(unit)
    
    local petUnit = string.gsub(unit, "party", "partypet")
    self:UpdatePet(petUnit)
end

function SFrames.Party:UpdatePortrait(unit)
    local data = self:GetFrameByUnit(unit)
    if not data then return end
    local f = data.frame
    f.portrait:SetUnit(unit)
    f.portrait:SetCamera(0)
    f.portrait:Hide()
    f.portrait:Show()
end

function SFrames.Party:UpdateHealth(unit)
    local data = self:GetFrameByUnit(unit)
    if not data then return end
    local f = data.frame
    
    if not UnitIsConnected(unit) then
        f.health:SetMinMaxValues(0, 100)
        f.health:SetValue(0)
        f.healthText:SetText("Offline")
        if f.health.healPredMine then f.health.healPredMine:Hide() end
        if f.health.healPredOther then f.health.healPredOther:Hide() end
        if f.offlineIcon then f.offlineIcon:Show() end
        return
    end
    if f.offlineIcon then f.offlineIcon:Hide() end
    
    local hp = UnitHealth(unit)
    local maxHp = UnitHealthMax(unit)
    f.health:SetMinMaxValues(0, maxHp)
    f.health:SetValue(hp)
    
    if maxHp > 0 then
        local percent = math.floor((hp / maxHp) * 100)
        f.healthText:SetText(percent .. "%")
    else
        f.healthText:SetText("")
    end

    self:UpdateHealPrediction(unit)
end

function SFrames.Party:UpdateHealPrediction(unit)
    local data = self:GetFrameByUnit(unit)
    if not data then return end
    local f = data.frame
    if not (f.health and f.health.healPredMine and f.health.healPredOther) then return end

    local predMine = f.health.healPredMine
    local predOther = f.health.healPredOther

    local function HidePredictions()
        predMine:Hide()
        predOther:Hide()
    end

    if not UnitExists(unit) or not UnitIsConnected(unit) then
        HidePredictions()
        return
    end

    local hp = UnitHealth(unit) or 0
    local maxHp = UnitHealthMax(unit) or 0
    if maxHp <= 0 or hp >= maxHp then
        HidePredictions()
        return
    end

    local _, mineIncoming, othersIncoming = GetIncomingHeals(unit)
    local missing = maxHp - hp
    if missing <= 0 then
        HidePredictions()
        return
    end

    local mineShown = math.min(math.max(0, mineIncoming), missing)
    local remaining = missing - mineShown
    local otherShown = math.min(math.max(0, othersIncoming), remaining)
    if mineShown <= 0 and otherShown <= 0 then
        HidePredictions()
        return
    end

    local barWidth = f.health:GetWidth() or 0
    if barWidth <= 0 then
        HidePredictions()
        return
    end

    local currentWidth = math.floor((hp / maxHp) * barWidth + 0.5)
    if currentWidth < 0 then currentWidth = 0 end
    if currentWidth > barWidth then currentWidth = barWidth end

    local availableWidth = barWidth - currentWidth
    if availableWidth <= 0 then
        HidePredictions()
        return
    end

    local mineWidth = math.floor((mineShown / maxHp) * barWidth + 0.5)
    local otherWidth = math.floor((otherShown / maxHp) * barWidth + 0.5)
    if mineWidth < 0 then mineWidth = 0 end
    if otherWidth < 0 then otherWidth = 0 end
    if mineWidth > availableWidth then mineWidth = availableWidth end
    if otherWidth > (availableWidth - mineWidth) then
        otherWidth = availableWidth - mineWidth
    end

    if mineWidth > 0 then
        predMine:ClearAllPoints()
        predMine:SetPoint("TOPLEFT", f.health, "TOPLEFT", currentWidth, 0)
        predMine:SetPoint("BOTTOMLEFT", f.health, "BOTTOMLEFT", currentWidth, 0)
        predMine:SetWidth(mineWidth)
        predMine:Show()
    else
        predMine:Hide()
    end

    if otherWidth > 0 then
        predOther:ClearAllPoints()
        predOther:SetPoint("TOPLEFT", f.health, "TOPLEFT", currentWidth + mineWidth, 0)
        predOther:SetPoint("BOTTOMLEFT", f.health, "BOTTOMLEFT", currentWidth + mineWidth, 0)
        predOther:SetWidth(otherWidth)
        predOther:Show()
    else
        predOther:Hide()
    end
end

function SFrames.Party:UpdatePowerType(unit)
    local data = self:GetFrameByUnit(unit)
    if not data then return end
    local f = data.frame
    
    local powerType = UnitPowerType(unit)
    local color = SFrames.Config.colors.power[powerType]
    if color then
        f.power:SetStatusBarColor(color.r, color.g, color.b)
    else
        f.power:SetStatusBarColor(0, 0, 1)
    end
end

function SFrames.Party:UpdatePower(unit)
    local data = self:GetFrameByUnit(unit)
    if not data then return end
    local f = data.frame
    
    if not UnitIsConnected(unit) then
        f.power:SetMinMaxValues(0, 100)
        f.power:SetValue(0)
        return
    end
    
    local power = UnitMana(unit)
    local maxPower = UnitManaMax(unit)
    f.power:SetMinMaxValues(0, maxPower)
    f.power:SetValue(power)
end

function SFrames.Party:UpdateRaidIcons()
    for i = 1, 4 do
        if self.frames[i] and self.frames[i].frame:IsShown() then
            self:UpdateRaidIcon(self.frames[i].unit)
        end
    end
end

function SFrames.Party:UpdateRaidIcon(unit)
    local data = self:GetFrameByUnit(unit)
    if not data then return end
    local f = data.frame
    if not f.raidIcon then return end
    if not GetRaidTargetIndex then
        f.raidIcon:Hide()
        return
    end
    if not UnitExists(unit) then
        f.raidIcon:Hide()
        return
    end
    local index = GetRaidTargetIndex(unit)
    if index and index > 0 and index <= 8 then
        local col = math.mod(index - 1, 4)
        local row = math.floor((index - 1) / 4)
        f.raidIcon:SetTexCoord(col * 0.25, (col + 1) * 0.25, row * 0.25, (row + 1) * 0.25)
        f.raidIcon:Show()
    else
        f.raidIcon:Hide()
    end
end

function SFrames.Party:UpdateAuras(unit)
    local data = self:GetFrameByUnit(unit)
    if not data then return end
    local f = data.frame

    local showDebuffs = not (SFramesDB and SFramesDB.partyShowDebuffs == false)
    local showBuffs = not (SFramesDB and SFramesDB.partyShowBuffs == false)
    
    local hasDebuff = false
    local debuffColor = {r=_A.slotBg[1], g=_A.slotBg[2], b=_A.slotBg[3]}
    
    SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
    
    -- Debuffs
    if showDebuffs then
        for i = 1, 4 do
            local texture, applications, debuffType = UnitDebuff(unit, i)
            local b = f.debuffs[i]
            b:SetID(i)
            
            if texture then
                if debuffType then
                    hasDebuff = true
                    if debuffType == "Magic" then debuffColor = {r=0.2, g=0.6, b=1}
                    elseif debuffType == "Curse" then debuffColor = {r=0.6, g=0, b=1}
                    elseif debuffType == "Disease" then debuffColor = {r=0.6, g=0.4, b=0}
                    elseif debuffType == "Poison" then debuffColor = {r=0, g=0.6, b=0}
                    end
                end
                
                b.icon:SetTexture(texture)
                
                SFrames.Tooltip:ClearLines()
                SFrames.Tooltip:SetUnitDebuff(unit, i)
                local timeLeft = SFrames:GetAuraTimeLeft(unit, i, false)
                SFrames.Tooltip:Hide()
                if timeLeft and timeLeft > 0 then
                    local newExp = GetTime() + timeLeft
                    if not b.expirationTime or math.abs(b.expirationTime - newExp) > 2 then
                        b.expirationTime = newExp
                    end
                    local currentLeft = b.expirationTime - GetTime()
                    if currentLeft > 0 and currentLeft < 3600 then
                        b.cdText:SetText(SFrames:FormatTime(currentLeft))
                    else
                        b.cdText:SetText("")
                    end
                else
                    b.expirationTime = nil
                    b.cdText:SetText("")
                end
                
                b:Show()
            else
                b.expirationTime = nil
                b.cdText:SetText("")
                b:Hide()
            end
        end
    else
        for i = 1, 4 do
            local b = f.debuffs[i]
            b.expirationTime = nil
            b.cdText:SetText("")
            b:Hide()
        end
    end
    
    if hasDebuff then
        f.health.bg:SetVertexColor(debuffColor.r, debuffColor.g, debuffColor.b, 1)
    else
        f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
    end
    
    -- Buffs
    if showBuffs then
        for i = 1, 4 do
            local texture = UnitBuff(unit, i)
            local b = f.buffs[i]
            b:SetID(i)
            
            if texture then
                b.icon:SetTexture(texture)
                
                SFrames.Tooltip:ClearLines()
                SFrames.Tooltip:SetUnitBuff(unit, i)
                local timeLeft = SFrames:GetAuraTimeLeft(unit, i, true)
                SFrames.Tooltip:Hide()
                if timeLeft and timeLeft > 0 then
                    local newExp = GetTime() + timeLeft
                    if not b.expirationTime or math.abs(b.expirationTime - newExp) > 2 then
                        b.expirationTime = newExp
                    end
                    local currentLeft = b.expirationTime - GetTime()
                    if currentLeft > 0 and currentLeft < 3600 then
                        b.cdText:SetText(SFrames:FormatTime(currentLeft))
                    else
                        b.cdText:SetText("")
                    end
                else
                    b.expirationTime = nil
                    b.cdText:SetText("")
                end
                
                b:Show()
            else
                b.expirationTime = nil
                b.cdText:SetText("")
                b:Hide()
            end
        end
    else
        for i = 1, 4 do
            local b = f.buffs[i]
            b.expirationTime = nil
            b.cdText:SetText("")
            b:Hide()
        end
    end
end

function SFrames.Party:TestMode()
    self.testing = not self.testing
    if self.testing then
        for i = 1, 4 do
            local data = self.frames[i]
            local f = data.frame
            f:Show()
            
            f.health:SetMinMaxValues(0, 100)
            f.health:SetValue(math.random(30, 90))
            f.health:SetStatusBarColor(SFrames.Config.colors.class["DRUID"].r, SFrames.Config.colors.class["DRUID"].g, SFrames.Config.colors.class["DRUID"].b)
            f.nameText:SetText("60 队友" .. i)
            f.nameText:SetTextColor(SFrames.Config.colors.class["DRUID"].r, SFrames.Config.colors.class["DRUID"].g, SFrames.Config.colors.class["DRUID"].b)
            f.healthText:SetText(math.floor(f.health:GetValue()) .. "%")
            if f.health.healPredMine then f.health.healPredMine:Hide() end
            if f.health.healPredOther then f.health.healPredOther:Hide() end
            
            f.power:SetMinMaxValues(0, 100)
            f.power:SetValue(math.random(20, 100))
            f.power:SetStatusBarColor(SFrames.Config.colors.power[0].r, SFrames.Config.colors.power[0].g, SFrames.Config.colors.power[0].b)
            
            f.leaderIcon:Hide()
            f.masterIcon:Hide()
            if i == 1 then
                f.leaderIcon:Show()
                f.masterIcon:Show()
            end
            
            -- Show one dummy debuff to test positioning
            f.debuffs[1].icon:SetTexture("Interface\\Icons\\Spell_Shadow_ShadowWordPain")
            f.debuffs[1]:Show()
            
            -- Test pet
            if f.petFrame then
                f.petFrame:Show()
                f.petFrame.nameText:SetText("测试宠物")
                f.petFrame.health:SetMinMaxValues(0, 100)
                f.petFrame.health:SetValue(100)
                f.petFrame.health:SetStatusBarColor(0.2, 0.8, 0.2)
            end
        end
    else
        self:UpdateAll()
        for i = 1, 4 do
            self.frames[i].frame.debuffs[1]:Hide()
        end
    end
end
