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

local CLASS_NAME_ZH = {
    WARRIOR = "\230\136\152\229\163\171",
    MAGE = "\230\179\149\229\184\136",
    ROGUE = "\230\189\156\232\161\140\232\128\133",
    DRUID = "\229\190\183\233\178\129\228\188\138",
    HUNTER = "\231\140\142\228\186\186",
    SHAMAN = "\232\144\168\230\187\161\231\165\173\229\143\184",
    PRIEST = "\231\137\167\229\184\136",
    WARLOCK = "\230\156\175\229\163\171",
    PALADIN = "\229\156\163\233\170\145\229\163\171",
}

local function GetChineseClassName(classToken, localizedClass)
    if classToken and CLASS_NAME_ZH[classToken] then
        return CLASS_NAME_ZH[classToken]
    end
    return localizedClass or ""
end

local function GetIncomingHeals(unit)
    if not (ShaguTweaks and ShaguTweaks.libpredict and ShaguTweaks.libpredict.UnitGetIncomingHeals) then
        return 0, 0, 0
    end

    local libpredict = ShaguTweaks.libpredict
    if libpredict.UnitGetIncomingHealsBreakdown then
        local ok, total, mine, others = pcall(function()
            return libpredict:UnitGetIncomingHealsBreakdown(unit, UnitName("player"))
        end)
        if ok then
            total = math.max(0, tonumber(total) or 0)
            mine = math.max(0, tonumber(mine) or 0)
            others = math.max(0, tonumber(others) or 0)
            return total, mine, others
        end
    end

    local ok, amount = pcall(function()
        return libpredict:UnitGetIncomingHeals(unit)
    end)
    if not ok then return 0, 0, 0 end

    amount = tonumber(amount) or 0
    if amount < 0 then amount = 0 end
    return amount, 0, amount
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.Player:GetConfig()
    local db = SFramesDB or {}

    local width = tonumber(db.playerFrameWidth) or SFrames.Config.width or 220
    width = Clamp(math.floor(width + 0.5), 170, 420)

    local portraitWidth = tonumber(db.playerPortraitWidth) or SFrames.Config.portraitWidth or 50
    portraitWidth = Clamp(math.floor(portraitWidth + 0.5), 32, 95)
    if portraitWidth > width - 90 then
        portraitWidth = width - 90
    end

    local healthHeight = tonumber(db.playerHealthHeight) or 38
    healthHeight = Clamp(math.floor(healthHeight + 0.5), 14, 80)

    local powerHeight = tonumber(db.playerPowerHeight) or 9
    powerHeight = Clamp(math.floor(powerHeight + 0.5), 6, 40)

    local height = healthHeight + powerHeight + 4
    height = Clamp(height, 30, 140)

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

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

    local frameScale = tonumber(db.playerFrameScale) or 1
    frameScale = Clamp(frameScale, 0.7, 1.8)

    return {
        width = width,
        height = height,
        portraitWidth = portraitWidth,
        healthHeight = healthHeight,
        powerHeight = powerHeight,
        nameFont = nameFont,
        valueFont = valueFont,
        scale = frameScale,
    }
end

function SFrames.Player:ApplyConfig()
    if not self.frame then return end

    local cfg = self:GetConfig()
    local f = self.frame
    local db = SFramesDB or {}

    local showPortrait = db.playerShowPortrait ~= false
    local frameAlpha = tonumber(db.playerFrameAlpha) or 1
    frameAlpha = Clamp(frameAlpha, 0.1, 1.0)

    f:SetScale(cfg.scale)
    f:SetWidth(cfg.width)
    f:SetHeight(cfg.height)
    f:SetAlpha(frameAlpha)

    if showPortrait then
        if f.portrait then
            f.portrait:SetWidth(cfg.portraitWidth)
            f.portrait:SetHeight(cfg.height - 2)
            f.portrait:Show()
        end
        if f.portraitBG then
            f.portraitBG:ClearAllPoints()
            f.portraitBG:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
            f.portraitBG:SetPoint("BOTTOMRIGHT", f.portrait, "BOTTOMRIGHT", 1, -1)
            f.portraitBG:Show()
        end
        if f.health then
            f.health:ClearAllPoints()
            f.health:SetPoint("TOPLEFT", f.portrait, "TOPRIGHT", 1, 0)
            f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -1)
            f.health:SetHeight(cfg.healthHeight)
        end
        if f.classIcon and f.classIcon.overlay then
            f.classIcon.overlay:ClearAllPoints()
            f.classIcon.overlay:SetPoint("CENTER", f.portrait, "TOPRIGHT", 0, 0)
        end
    else
        if f.portrait then f.portrait:Hide() end
        if f.portraitBG then f.portraitBG:Hide() end
        if f.health then
            f.health:ClearAllPoints()
            f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1)
            f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -1)
            f.health:SetHeight(cfg.healthHeight)
        end
        if f.classIcon and f.classIcon.overlay then
            f.classIcon.overlay:ClearAllPoints()
            f.classIcon.overlay:SetPoint("CENTER", f, "TOPLEFT", 8, 0)
        end
    end

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

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

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

    if f.restOverlay then
        if showPortrait then f.restOverlay:SetAlpha(1) else f.restOverlay:SetAlpha(0) end
    end

    if f.castbar then
        f.castbar:ClearAllPoints()
        if showPortrait then
            f.castbar:SetPoint("BOTTOMRIGHT", f, "TOPRIGHT", 0, 6)
            f.castbar:SetPoint("BOTTOMLEFT", f.portrait, "TOPLEFT", SFrames.Config.castbarHeight + 6, 6)
        else
            f.castbar:SetPoint("BOTTOMRIGHT", f, "TOPRIGHT", 0, 6)
            f.castbar:SetPoint("BOTTOMLEFT", f, "TOPLEFT", SFrames.Config.castbarHeight + 6, 6)
        end
    end

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

    if f.nameText then
        f.nameText:SetFont(fontPath, cfg.nameFont, outline)
    end
    if f.healthText then
        f.healthText:SetFont(fontPath, cfg.valueFont, outline)
    end
    if f.powerText then
        f.powerText:SetFont(fontPath, cfg.valueFont, outline)
    end
    if f.manaText then
        local manaFont = cfg.valueFont - 1
        if manaFont < 8 then manaFont = 8 end
        f.manaText:SetFont(fontPath, manaFont, outline)
    end

    if f.zLetters then
        for i = 1, 3 do
            if f.zLetters[i] and f.zLetters[i].text then
                f.zLetters[i].text:SetFont(fontPath, 8 + (i - 1) * 3, "OUTLINE")
            end
        end
    end

    self:UpdateAll()
end

function SFrames.Player:Initialize()
    local f = CreateFrame("Button", "SFramesPlayerFrame", UIParent)
    f:SetWidth(SFrames.Config.width)
    f:SetHeight(SFrames.Config.height)
    if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["PlayerFrame"] then
        local pos = SFramesDB.Positions["PlayerFrame"]
        f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs, pos.yOfs)
    else
        f:SetPoint("CENTER", UIParent, "CENTER", -200, -100)
    end
    local frameScale = (SFramesDB and type(SFramesDB.playerFrameScale) == "number") and SFramesDB.playerFrameScale or 1
    f:SetScale(frameScale)
    
    -- Make it movable
    f:SetMovable(true)
    f:EnableMouse(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", function() if IsAltKeyDown() or SFrames.isUnlocked then f:StartMoving() end end)
    f:SetScript("OnDragStop", function()
        f:StopMovingOrSizing()
        if not SFramesDB then SFramesDB = {} end
        if not SFramesDB.Positions then SFramesDB.Positions = {} end
        local point, relativeTo, relativePoint, xOfs, yOfs = f:GetPoint()
        SFramesDB.Positions["PlayerFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs }
    end)
    
    -- Register clicks for targeting
    f:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    f:SetScript("OnClick", function()
        if arg1 == "LeftButton" then
            TargetUnit("player")
        else
            ToggleDropDownMenu(1, nil, PlayerFrameDropDown, this:GetName(), 106, 27)
        end
    end)
    
    -- Base Backdrop
    SFrames:CreateUnitBackdrop(f)
    
    -- 3D Portrait
    local pWidth = SFrames.Config.portraitWidth
    f.portrait = CreateFrame("PlayerModel", nil, f)
    f.portrait:SetWidth(pWidth)
    f.portrait:SetHeight(SFrames.Config.height - 2)
    f.portrait:SetPoint("LEFT", f, "LEFT", 1, 0)
    f.portrait:SetUnit("player")
    f.portrait:SetCamera(0)
    f.portrait:SetPosition(-1.0, 0, 0)
    
    -- We need a backdrop for the portrait to separate it from health bar
    local pbg = CreateFrame("Frame", nil, f)
    pbg:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
    pbg:SetPoint("BOTTOMRIGHT", f.portrait, "BOTTOMRIGHT", 1, -1)
    pbg:SetFrameLevel(f:GetFrameLevel())
    SFrames:CreateUnitBackdrop(pbg)
    f.portraitBG = pbg
    
    -- Health Bar
    f.health = SFrames:CreateStatusBar(f, "SFramesPlayerHealth")
    f.health:SetPoint("TOPLEFT", f.portrait, "TOPRIGHT", 1, 0)
    f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -1)
    f.health:SetHeight((SFrames.Config.height - 2) * 0.82 - 1) -- 82% height, minus 1px gap
    f.health:SetMinMaxValues(0, 100)
    
    -- Health Backdrop
    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
    
    -- Add a dark backdrop behind the health texture
    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, "SFramesPlayerPower")
    f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1)
    f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1)
    f.power:SetMinMaxValues(0, 100)
    
    -- Power Backdrop
    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
    
    -- Add a dark backdrop behind the power texture
    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)

    -- Five-second rule ticker (mana regen delay indicator)
    f.power.fsrGlow = f.power:CreateTexture(nil, "OVERLAY")
    f.power.fsrGlow:SetTexture("Interface\\CastingBar\\UI-CastingBar-Spark")
    f.power.fsrGlow:SetVertexColor(0.62, 0.90, 1.0, 0.52)
    f.power.fsrGlow:SetBlendMode("ADD")
    pcall(function() f.power.fsrGlow:SetDrawLayer("OVERLAY", 5) end)
    f.power.fsrGlow:Hide()

    -- Texts
    f.nameText = SFrames:CreateFontString(f.health, 10, "LEFT")
    f.nameText:SetPoint("LEFT", f.health, "LEFT", 6, 0)
    
    f.healthText = SFrames:CreateFontString(f.health, 10, "RIGHT")
    f.healthText:SetPoint("RIGHT", f.health, "RIGHT", -6, 0)

    f.powerText = SFrames:CreateFontString(f.power, 10, "RIGHT")
    f.powerText:SetPoint("RIGHT", f.power, "RIGHT", -6, 0)

    -- Extra mana text for shapeshift druids (show blue mana while rage/energy is active)
    f.manaText = SFrames:CreateFontString(f.power, 9, "LEFT")
    f.manaText:SetPoint("LEFT", f.power, "LEFT", 6, 0)
    f.manaText:SetTextColor(0.30, 0.65, 1.0)
    f.manaText:Hide()

    f.manaBar = CreateFrame("StatusBar", nil, f)
    f.manaBar:SetHeight(4)
    f.manaBar:SetPoint("TOPLEFT", f.power, "BOTTOMLEFT", 0, -1)
    f.manaBar:SetPoint("TOPRIGHT", f.power, "BOTTOMRIGHT", 0, -1)
    f.manaBar:SetStatusBarTexture(SFrames:GetTexture())
    f.manaBar:SetStatusBarColor(0.30, 0.65, 1.0, 0.90)
    f.manaBar:SetMinMaxValues(0, 100)
    f.manaBar:SetValue(0)
    f.manaBar:SetFrameLevel(f:GetFrameLevel() + 1)
    f.manaBar.bg = f.manaBar:CreateTexture(nil, "BACKGROUND")
    f.manaBar.bg:SetAllPoints()
    f.manaBar.bg:SetTexture(SFrames:GetTexture())
    f.manaBar.bg:SetVertexColor(0.05, 0.10, 0.20, 0.7)
    f.manaBar:Hide()

    -- Outline/shadow setup for text to make it pop
    f.nameText:SetShadowColor(0, 0, 0, 1)
    f.nameText:SetShadowOffset(1, -1)
    f.healthText:SetShadowColor(0, 0, 0, 1)
    f.healthText:SetShadowOffset(1, -1)
    f.powerText:SetShadowColor(0, 0, 0, 1)
    f.powerText:SetShadowOffset(1, -1)
    f.manaText:SetShadowColor(0, 0, 0, 1)
    f.manaText:SetShadowOffset(1, -1)
    
    -- Resting Indicator (animated zzz on portrait)
    local restOverlay = CreateFrame("Frame", nil, f)
    restOverlay:SetFrameLevel((f:GetFrameLevel() or 0) + 6)
    restOverlay:SetWidth(pWidth)
    restOverlay:SetHeight(SFrames.Config.height)
    restOverlay:SetPoint("CENTER", f.portrait, "CENTER", 0, 0)
    f.restOverlay = restOverlay

    local zLetters = {}
    for i = 1, 3 do
        local zf = CreateFrame("Frame", nil, restOverlay)
        zf:SetWidth(16)
        zf:SetHeight(16)
        local zt = zf:CreateFontString(nil, "OVERLAY")
        zt:SetFont(SFrames:GetFont(), 8 + (i - 1) * 3, "OUTLINE")
        zt:SetText("z")
        zt:SetTextColor(0.85, 0.85, 1.0)
        zt:SetShadowColor(0, 0, 0, 0.8)
        zt:SetShadowOffset(1, -1)
        zt:SetAllPoints(zf)
        zf.text = zt
        zf.phase = (i - 1) * 1.2
        zf.baseX = (i - 1) * 6 - 2
        zf.baseY = (i - 1) * 5
        zf:SetPoint("BOTTOMLEFT", restOverlay, "CENTER", zf.baseX, zf.baseY - 4)
        zLetters[i] = zf
    end
    f.zLetters = zLetters

    local restElapsed = 0
    restOverlay:SetScript("OnUpdate", function()
        restElapsed = restElapsed + arg1
        for idx = 1, 3 do
            local zf = zLetters[idx]
            local t = math.mod(restElapsed + zf.phase, 3.6)
            local ratio = t / 3.6
            local floatY = ratio * 14
            local alpha
            if ratio < 0.15 then
                alpha = ratio / 0.15
            elseif ratio < 0.7 then
                alpha = 1.0
            else
                alpha = 1.0 - (ratio - 0.7) / 0.3
            end
            if alpha < 0 then alpha = 0 end
            if alpha > 1 then alpha = 1 end
            zf.text:SetAlpha(alpha)
            zf:ClearAllPoints()
            zf:SetPoint("BOTTOMLEFT", restOverlay, "CENTER", zf.baseX, zf.baseY - 4 + floatY)
        end
    end)
    restOverlay:Hide()

    -- Class Icon Badge (overlaid on portrait, top-right corner with 1/3 outside)
    f.classIcon = SFrames:CreateClassIcon(f, 16)
    f.classIcon.overlay:SetPoint("CENTER", f.portrait, "TOPRIGHT", 0, 0)

    -- Party Leader Icon
    local leaderOvr = CreateFrame("Frame", nil, f)
    leaderOvr:SetFrameLevel((f:GetFrameLevel() or 0) + 4)
    leaderOvr:SetAllPoints(f)
    f.leaderIcon = leaderOvr:CreateTexture(nil, "OVERLAY")
    f.leaderIcon:SetTexture("Interface\\GroupFrame\\UI-Group-LeaderIcon")
    f.leaderIcon:SetWidth(16)
    f.leaderIcon:SetHeight(16)
    f.leaderIcon:SetPoint("TOPLEFT", f.portrait, "TOPLEFT", -4, 4)
    f.leaderIcon:Hide()

    -- Raid Target Icon (top center of health bar, half outside frame)
    local raidIconSize = 22
    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

    self.frame = f
    self:ApplyConfig()
    self.frame:Show() -- Ensure it's explicitly shown
    self:UpdateAll()
    
    -- Events
    SFrames:RegisterEvent("UNIT_HEALTH", function() if arg1 == "player" then self:UpdateHealth() end end)
    SFrames:RegisterEvent("UNIT_MAXHEALTH", function() if arg1 == "player" then self:UpdateHealth() end end)
    SFrames:RegisterEvent("UNIT_MANA", function() if arg1 == "player" then self:UpdatePower() end end)
    SFrames:RegisterEvent("UNIT_MAXMANA", function() if arg1 == "player" then self:UpdatePower() end end)
    SFrames:RegisterEvent("UNIT_ENERGY", function() if arg1 == "player" then self:UpdatePower() end end)
    SFrames:RegisterEvent("UNIT_MAXENERGY", function() if arg1 == "player" then self:UpdatePower() end end)
    SFrames:RegisterEvent("UNIT_RAGE", function() if arg1 == "player" then self:UpdatePower() end end)
    SFrames:RegisterEvent("UNIT_MAXRAGE", function() if arg1 == "player" then self:UpdatePower() end end)
    SFrames:RegisterEvent("PLAYER_ENTERING_WORLD", function() self:UpdateAll() end)
    SFrames:RegisterEvent("PLAYER_LEVEL_UP", function() 
        if arg1 then self.currentLevel = arg1 end
        self:UpdateAll()
        if arg1 and mod(arg1, 2) == 0 and (not SFramesDB or SFramesDB.trainerReminder ~= false) then
            self:ShowTrainerReminder(arg1)
        end
    end)
    SFrames:RegisterEvent("TRAINER_SHOW", function()
        SFrames.Player.trainerScannedThisVisit = nil
        SFrames.Player.trainerShowPending = true
        SFrames.Player.trainerRetryCount = 0
        if not SFrames.Player.trainerRetryFrame then
            SFrames.Player.trainerRetryFrame = CreateFrame("Frame")
        end
        SFrames.Player.trainerRetryFrame:SetScript("OnUpdate", function()
            if not this.elapsed then this.elapsed = 0 end
            this.elapsed = this.elapsed + arg1
            if this.elapsed < 0.3 then return end
            this.elapsed = 0
            if SFrames.Player.trainerScannedThisVisit then
                this:SetScript("OnUpdate", nil)
                return
            end
            SFrames.Player.trainerRetryCount = (SFrames.Player.trainerRetryCount or 0) + 1
            if SFrames.Player.trainerRetryCount > 10 then
                SFrames.Player:ScanTrainer()
                this:SetScript("OnUpdate", nil)
                return
            end
            SFrames.Player:ScanTrainer()
        end)
    end)
    SFrames:RegisterEvent("TRAINER_UPDATE", function()
        if not SFrames.Player.trainerScannedThisVisit then
            SFrames.Player:ScanTrainer()
        end
    end)
    SFrames:RegisterEvent("PARTY_MEMBERS_CHANGED", function() self:UpdateLeaderIcon() end)
    SFrames:RegisterEvent("PARTY_LEADER_CHANGED", function() self:UpdateLeaderIcon() end)
    SFrames:RegisterEvent("RAID_TARGET_UPDATE", function() self:UpdateRaidIcon() end)
    SFrames:RegisterEvent("UNIT_PORTRAIT_UPDATE", function()
        if arg1 == "player" and self.frame.portrait and not (SFramesDB and SFramesDB.playerShowPortrait == false) then
            self.frame.portrait:SetUnit("player")
            self.frame.portrait:SetCamera(0)
            self.frame.portrait:SetPosition(-1.0, 0, 0)
        end
    end)
    SFrames:RegisterEvent("UNIT_DISPLAYPOWER", function() if arg1 == "player" then self:UpdatePowerType(); self:UpdatePower() end end)
    SFrames:RegisterEvent("UPDATE_SHAPESHIFT_FORM", function() self:UpdatePowerType(); self:UpdatePower() end)
    SFrames:RegisterEvent("PLAYER_UPDATE_RESTING", function() self:UpdateRestingStatus() end)
    
    f.unit = "player"
    f:SetScript("OnEnter", function()
        GameTooltip_SetDefaultAnchor(GameTooltip, this)
        GameTooltip:SetUnit(this.unit)
        GameTooltip:Show()
    end)
    f:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
end

function SFrames.Player:HasSpellInBook(spellName)
    local baseName = string.gsub(spellName, " 等级 %d+$", "")
    baseName = string.gsub(baseName, " %d+级$", "")
    local i = 1
    while true do
        local name = GetSpellName(i, BOOKTYPE_SPELL)
        if not name then return false end
        if name == spellName or name == baseName then return true end
        i = i + 1
    end
end

function SFrames.Player:GetSpellIcon(skillDisplayName)
    local baseName = string.gsub(skillDisplayName, " %d+级$", "")
    local altName1 = string.gsub(baseName, "：", ":")
    local altName2 = string.gsub(baseName, ":", "：")
    local i = 1
    while true do
        local name = GetSpellName(i, BOOKTYPE_SPELL)
        if not name then break end
        if name == baseName or name == altName1 or name == altName2 then
            return GetSpellTexture(i, BOOKTYPE_SPELL)
        end
        i = i + 1
    end
    return nil
end

function SFrames.Player:ParseTrainerTooltipLevel(serviceIndex)
    if not self.trainerScanTip then
        local tt = CreateFrame("GameTooltip", "NanamiTrainerScanTip", nil, "GameTooltipTemplate")
        tt:SetOwner(UIParent, "ANCHOR_NONE")
        self.trainerScanTip = tt
    end
    local tt = self.trainerScanTip
    tt:ClearLines()
    if not tt.SetTrainerService then return nil end
    tt:SetTrainerService(serviceIndex)
    for j = 2, tt:NumLines() do
        local textObj = getglobal("NanamiTrainerScanTipTextLeft" .. j)
        if textObj then
            local text = textObj:GetText()
            if text then
                local _, _, lvl = string.find(text, "需要等级%s*(%d+)")
                if not lvl then
                    _, _, lvl = string.find(text, "Requires Level (%d+)")
                end
                if lvl then return tonumber(lvl) end
            end
        end
    end
    return nil
end

function SFrames.Player:FindSkillLevelInStaticData(classEn, skillName)
    local staticData = SFrames.ClassSkillData and SFrames.ClassSkillData[classEn]
    if not staticData then return nil end
    local baseName = string.gsub(skillName, " %d+级$", "")
    baseName = string.gsub(baseName, "（等级 %d+）$", "")
    baseName = string.gsub(baseName, "%s+$", "")
    for level, skills in pairs(staticData) do
        for _, s in ipairs(skills) do
            local sBase = string.gsub(s, " %d+级$", "")
            sBase = string.gsub(sBase, "（等级 %d+）$", "")
            sBase = string.gsub(sBase, "%s+$", "")
            if baseName == sBase or skillName == s then
                return level
            end
        end
    end
    local talentData = SFrames.TalentTrainerSkills and SFrames.TalentTrainerSkills[classEn]
    if talentData then
        for level, entries in pairs(talentData) do
            for _, entry in ipairs(entries) do
                local tBase = string.gsub(entry[1], " %d+级$", "")
                if baseName == tBase or skillName == entry[1] then
                    return level
                end
            end
        end
    end
    return nil
end

function SFrames.Player:ScanTrainer()
    if self.scanningTrainer then return end
    self.scanningTrainer = true

    local hex = SFrames.Theme and SFrames.Theme:GetAccentHex() or "ffffb3d9"

    if not GetNumTrainerServices then
        DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cffff6666训练师扫描失败: GetNumTrainerServices 不存在|r")
        self.scanningTrainer = nil
        return
    end
    if IsTradeskillTrainer and IsTradeskillTrainer() then
        self.scanningTrainer = nil
        return
    end

    if SetTrainerServiceTypeFilter then
        SetTrainerServiceTypeFilter("available", 1)
        SetTrainerServiceTypeFilter("unavailable", 1)
        SetTrainerServiceTypeFilter("used", 1)
    end

    local _, classEn = UnitClass("player")
    if not classEn or not SFramesDB then self.scanningTrainer = nil return end

    local cache = {}
    local numServices = GetNumTrainerServices()
    if not numServices or numServices == 0 then
        DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cffff6666训练师扫描: 未检测到技能列表 (0项)，将在数据加载后重试|r")
        self.scanningTrainer = nil
        return
    end

    local hasLevelAPI = (GetTrainerServiceLevelReq ~= nil)
    local noLevelCount = 0
    local totalAdded = 0

    local iconMissCount = 0

    for i = 1, numServices do
        local name, subText, serviceType = GetTrainerServiceInfo(i)

        if name and subText and subText ~= "" then
            local icon = GetTrainerServiceIcon and GetTrainerServiceIcon(i)

            if (not icon or icon == "") and ClassTrainerFrame then
                local skillButton = getglobal("ClassTrainerSkill" .. i)
                if skillButton then
                    local iconTex = getglobal("ClassTrainerSkill" .. i .. "Icon")
                    if iconTex and iconTex.GetTexture then
                        icon = iconTex:GetTexture()
                    end
                end
            end

            if not icon or icon == "" then
                iconMissCount = iconMissCount + 1
            end

            local reqLevel
            if hasLevelAPI then
                reqLevel = GetTrainerServiceLevelReq(i)
            end
            if not reqLevel or reqLevel == 0 then
                reqLevel = self:ParseTrainerTooltipLevel(i)
            end
            if not reqLevel or reqLevel == 0 then
                local lookupName = name .. " " .. subText
                reqLevel = self:FindSkillLevelInStaticData(classEn, lookupName)
                if not reqLevel then
                    reqLevel = self:FindSkillLevelInStaticData(classEn, name)
                end
            end
            if not reqLevel or reqLevel == 0 then
                noLevelCount = noLevelCount + 1
            end

            if reqLevel and reqLevel > 0 then
                local displayName = name .. " " .. subText
                if not cache[reqLevel] then
                    cache[reqLevel] = {}
                end
                table.insert(cache[reqLevel], {
                    name = displayName,
                    icon = icon or "",
                })
                totalAdded = totalAdded + 1
            end
        end
    end

    if not SFramesDB.trainerCache then
        SFramesDB.trainerCache = {}
    end
    SFramesDB.trainerCache[classEn] = cache
    self.trainerScannedThisVisit = true

    local levelCount = 0
    for _ in pairs(cache) do levelCount = levelCount + 1 end
    local iconOk = totalAdded - iconMissCount
    DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r 已从训练师更新技能数据（" .. levelCount .. " 个等级，" .. totalAdded .. " 项技能，" .. iconOk .. " 个图标）")
    if noLevelCount > 0 then
        DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cffff9900有 " .. noLevelCount .. " 项技能无法确定等级要求，已跳过|r")
    end
    if iconMissCount > 0 then
        DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cffff9900有 " .. iconMissCount .. " 项技能未获取到图标|r")
    end

    self.scanningTrainer = nil
end

function SFrames.Player:ShowTrainerReminder(newLevel)
    local _, classEn = UnitClass("player")
    local classNames = {
        WARRIOR = "战士", PALADIN = "圣骑士", HUNTER = "猎人",
        ROGUE = "盗贼", PRIEST = "牧师", SHAMAN = "萨满祭司",
        MAGE = "法师", WARLOCK = "术士", DRUID = "德鲁伊",
    }
    local className = classNames[classEn] or UnitClass("player")

    local classFallbackIcons = {
        WARRIOR = "Interface\\Icons\\Ability_Warrior_OffensiveStance",
        PALADIN = "Interface\\Icons\\Spell_Holy_HolyBolt",
        HUNTER = "Interface\\Icons\\Ability_Marksmanship",
        ROGUE = "Interface\\Icons\\Ability_BackStab",
        PRIEST = "Interface\\Icons\\Spell_Holy_HolyBolt",
        SHAMAN = "Interface\\Icons\\Spell_Nature_Lightning",
        MAGE = "Interface\\Icons\\Spell_Frost_IceStorm",
        WARLOCK = "Interface\\Icons\\Spell_Shadow_DeathCoil",
        DRUID = "Interface\\Icons\\Spell_Nature_Regeneration",
    }
    local fallbackIcon = classFallbackIcons[classEn] or "Interface\\Icons\\Trade_Engraving"

    local allSkills = {}
    local allIcons = {}
    local talentSkills = {}
    local usedCache = false

    local classCache = SFramesDB and SFramesDB.trainerCache and SFramesDB.trainerCache[classEn]
    if classCache then
        local lowLevel = newLevel - 1
        if lowLevel < 1 then lowLevel = 1 end
        for lv = lowLevel, newLevel do
            if classCache[lv] then
                usedCache = true
                for _, entry in ipairs(classCache[lv]) do
                    if not self:HasSpellInBook(entry.name) then
                        table.insert(allSkills, entry.name)
                        local ico = entry.icon
                        if not ico or ico == "" then
                            ico = self:GetSpellIcon(entry.name) or fallbackIcon
                        end
                        table.insert(allIcons, ico)
                    end
                end
            end
        end
    end

    if not usedCache then
        local baseSkills = SFrames.ClassSkillData and SFrames.ClassSkillData[classEn] and SFrames.ClassSkillData[classEn][newLevel]
        if baseSkills then
            for _, s in ipairs(baseSkills) do
                table.insert(allSkills, s)
                table.insert(allIcons, self:GetSpellIcon(s) or fallbackIcon)
            end
        end

        local talentData = SFrames.TalentTrainerSkills and SFrames.TalentTrainerSkills[classEn] and SFrames.TalentTrainerSkills[classEn][newLevel]
        if talentData then
            for _, entry in ipairs(talentData) do
                local displayName = entry[1]
                local requiredSpell = entry[2]
                if self:HasSpellInBook(requiredSpell) then
                    table.insert(allSkills, displayName)
                    table.insert(allIcons, self:GetSpellIcon(displayName) or fallbackIcon)
                    table.insert(talentSkills, displayName)
                end
            end
        end
    end

    local mountQuest = SFrames.ClassMountQuests and SFrames.ClassMountQuests[classEn] and SFrames.ClassMountQuests[classEn][newLevel]

    local skillCount = table.getn(allSkills)

    if skillCount == 0 and not mountQuest then
        return
    end

    local hex = SFrames.Theme and SFrames.Theme:GetAccentHex() or "ffffb3d9"

    SFrames:Print(string.format("已达到 %d 级！你的%s训练师有新技能可以学习。", newLevel, className))
    if skillCount > 0 then
        DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r 可学习的技能（共 " .. skillCount .. " 项）：")
        local line = "  "
        local lineCount = 0
        for i = 1, skillCount do
            if lineCount > 0 then line = line .. ", " end
            line = line .. "|cffffd100" .. allSkills[i] .. "|r"
            lineCount = lineCount + 1
            if i == skillCount or lineCount == 4 then
                DEFAULT_CHAT_FRAME:AddMessage(line)
                line = "  "
                lineCount = 0
            end
        end
    end
    if not usedCache and table.getn(talentSkills) > 0 then
        DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cff00ff00(天赋)|r " .. table.concat(talentSkills, ", "))
    end
    if mountQuest then
        DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cffffff00★|r " .. mountQuest)
    end

    local bannerMsg = string.format("Lv.%d - %s训练师有 %d 项新技能可学习", newLevel, className, skillCount)
    if mountQuest then
        bannerMsg = bannerMsg .. " + " .. mountQuest
    end
    if skillCount == 0 and not mountQuest then
        bannerMsg = string.format("Lv.%d - 前往%s训练师查看可学技能", newLevel, className)
    elseif skillCount == 0 and mountQuest then
        bannerMsg = string.format("Lv.%d - %s", newLevel, mountQuest)
    end
    UIErrorsFrame:AddMessage(bannerMsg, 1.0, 0.82, 0.0, 1, 5)
    PlaySound("LEVELUP")

    if not self.trainerReminderFrame then
        local fr = CreateFrame("Frame", "NanamiTrainerReminder", UIParent)
        fr:SetWidth(440)
        fr:SetHeight(106)
        fr:SetPoint("TOP", UIParent, "TOP", 0, -120)
        fr:SetFrameStrata("DIALOG")

        local bg = fr:CreateTexture(nil, "BACKGROUND")
        bg:SetAllPoints(fr)
        bg:SetTexture(0, 0, 0, 0.78)

        local border = fr:CreateTexture(nil, "BORDER")
        border:SetPoint("TOPLEFT", fr, "TOPLEFT", -1, 1)
        border:SetPoint("BOTTOMRIGHT", fr, "BOTTOMRIGHT", 1, -1)
        border:SetTexture(1, 0.82, 0, 0.3)

        local icon = fr:CreateTexture(nil, "ARTWORK")
        icon:SetWidth(36)
        icon:SetHeight(36)
        icon:SetPoint("TOPLEFT", fr, "TOPLEFT", 10, -6)
        icon:SetTexture("Interface\\Icons\\INV_Misc_Book_11")
        fr.icon = icon

        local title = fr:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        title:SetPoint("TOPLEFT", icon, "TOPRIGHT", 10, -2)
        title:SetPoint("RIGHT", fr, "RIGHT", -10, 0)
        title:SetJustifyH("LEFT")
        title:SetTextColor(1, 0.82, 0)
        fr.title = title

        local subtitle = fr:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        subtitle:SetPoint("TOPLEFT", icon, "TOPRIGHT", 10, -18)
        subtitle:SetPoint("RIGHT", fr, "RIGHT", -10, 0)
        subtitle:SetJustifyH("LEFT")
        subtitle:SetTextColor(0.75, 0.75, 0.75)
        fr.subtitle = subtitle

        fr.skillIcons = {}
        fr.skillBorders = {}
        local maxIcons = 13
        for idx = 1, maxIcons do
            local bdr = fr:CreateTexture(nil, "BORDER")
            bdr:SetWidth(30)
            bdr:SetHeight(30)
            bdr:SetPoint("TOPLEFT", fr, "TOPLEFT", 9 + (idx - 1) * 32, -45)
            bdr:SetTexture(1, 0.82, 0, 0.25)
            bdr:Hide()
            fr.skillBorders[idx] = bdr

            local si = fr:CreateTexture(nil, "ARTWORK")
            si:SetWidth(28)
            si:SetHeight(28)
            si:SetPoint("CENTER", bdr, "CENTER", 0, 0)
            si:SetTexCoord(0.07, 0.93, 0.07, 0.93)
            si:Hide()
            fr.skillIcons[idx] = si
        end

        local detail = fr:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        detail:SetPoint("BOTTOMLEFT", fr, "BOTTOMLEFT", 10, 8)
        detail:SetPoint("RIGHT", fr, "RIGHT", -10, 0)
        detail:SetJustifyH("LEFT")
        detail:SetTextColor(0.85, 0.85, 0.85)
        fr.detail = detail

        fr:SetAlpha(0)
        fr:Hide()
        self.trainerReminderFrame = fr
    end

    local fr = self.trainerReminderFrame

    for idx = 1, 13 do
        fr.skillIcons[idx]:Hide()
        fr.skillBorders[idx]:Hide()
    end

    local iconCount = 0

    for i = 1, skillCount do
        if iconCount >= 13 then break end
        iconCount = iconCount + 1
        fr.skillIcons[iconCount]:SetTexture(allIcons[i])
        fr.skillIcons[iconCount]:Show()
        fr.skillBorders[iconCount]:Show()
    end
    if mountQuest and iconCount < 13 then
        iconCount = iconCount + 1
        fr.skillIcons[iconCount]:SetTexture("Interface\\Icons\\Spell_Nature_Swiftness")
        fr.skillIcons[iconCount]:Show()
        fr.skillBorders[iconCount]:Show()
    end

    fr.title:SetText(string.format("已达到 |cffffffff%d|r 级 — %s训练师有新技能", newLevel, className))

    if skillCount > 0 or mountQuest then
        local preview = ""
        if skillCount > 0 then
            preview = "|cffffd100" .. allSkills[1] .. "|r"
            if skillCount > 1 then preview = preview .. ", |cffffd100" .. allSkills[2] .. "|r" end
            if skillCount > 2 then preview = preview .. ", |cffffd100" .. allSkills[3] .. "|r" end
            if skillCount > 3 then preview = preview .. " 等 " .. skillCount .. " 项" end
        end
        if mountQuest then
            if preview ~= "" then preview = preview .. " | " end
            preview = preview .. "|cffffff00" .. mountQuest .. "|r"
        end
        fr.subtitle:SetText(preview)
        fr.detail:SetText("详见聊天窗口")
    else
        fr.subtitle:SetText("")
        fr.detail:SetText("前往职业训练师查看可学习的技能")
    end

    if iconCount > 0 then
        fr:SetHeight(106)
    else
        fr:SetHeight(80)
    end
    fr:Show()
    fr:SetAlpha(0)

    fr.fadeState = "in"
    fr.fadeTimer = 0
    fr.holdTimer = 0
    fr:SetScript("OnUpdate", function()
        local dt = arg1
        if fr.fadeState == "in" then
            fr.fadeTimer = fr.fadeTimer + dt
            local a = fr.fadeTimer / 0.5
            if a >= 1 then
                a = 1
                fr.fadeState = "hold"
            end
            fr:SetAlpha(a)
        elseif fr.fadeState == "hold" then
            fr.holdTimer = fr.holdTimer + dt
            if fr.holdTimer >= 8 then
                fr.fadeState = "out"
                fr.fadeTimer = 0
            end
        elseif fr.fadeState == "out" then
            fr.fadeTimer = fr.fadeTimer + dt
            local a = 1 - fr.fadeTimer / 1.0
            if a <= 0 then
                a = 0
                fr:Hide()
                fr:SetScript("OnUpdate", nil)
            end
            fr:SetAlpha(a)
        end
    end)
end

function SFrames.Player:UpdateAll()
    if not self.frame then return end
    self:UpdateHealth()
    self:UpdatePowerType()
    self:UpdatePower()
    self:UpdateLeaderIcon()
    self:UpdateRaidIcon()
    self:UpdateRestingStatus()
    
    local name = UnitName("player") or ""
    -- Use the stored level from PLAYER_LEVEL_UP if it exists, since API might lag slightly
    local level = self.currentLevel or UnitLevel("player")
    local formattedLevel = string.format("|cffffff00%d|r", level)
    
    if SFramesDB and SFramesDB.showLevel == false then
        formattedLevel = ""
    else
        formattedLevel = formattedLevel .. " "
    end
    
    local showPortrait = not (SFramesDB and SFramesDB.playerShowPortrait == false)
    if showPortrait and self.frame.portrait then
        self.frame.portrait:SetUnit("player")
        self.frame.portrait:SetCamera(0)
        self.frame.portrait:SetPosition(-1.0, 0, 0)
    end
    
    -- Class Color for Health
    local localizedClass, class = UnitClass("player")
    local className = GetChineseClassName(class, localizedClass)
    local nameLine = formattedLevel .. name
    local showClassText = not (SFramesDB and SFramesDB.playerShowClass == false)
    if showClassText and className and className ~= "" then
        nameLine = nameLine .. " " .. className
    end

    if not (SFramesDB and SFramesDB.playerShowClassIcon == false) then
        SFrames:SetClassIcon(self.frame.classIcon, class)
    else
        self.frame.classIcon:Hide()
        if self.frame.classIcon.overlay then self.frame.classIcon.overlay:Hide() end
    end

    local useClassColor = not (SFramesDB and SFramesDB.classColorHealth == false)

    if useClassColor and class and SFrames.Config.colors.class[class] then
        local color = SFrames.Config.colors.class[class]
        self.frame.health:SetStatusBarColor(color.r, color.g, color.b)
        
        -- Apply Class Color to Name
        self.frame.nameText:SetText(nameLine)
        self.frame.nameText:SetTextColor(color.r, color.g, color.b)
    else
        self.frame.health:SetStatusBarColor(0, 1, 0)
        self.frame.nameText:SetText(nameLine)
        self.frame.nameText:SetTextColor(1, 1, 1)
    end
end

function SFrames.Player:UpdateRestingStatus()
    if not self.frame or not self.frame.restOverlay then return end
    if IsResting() then
        self.frame.restOverlay:Show()
    else
        self.frame.restOverlay:Hide()
    end
end

function SFrames.Player:UpdateLeaderIcon()
    if IsPartyLeader() then
        self.frame.leaderIcon:Show()
    else
        self.frame.leaderIcon:Hide()
    end
end

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

function SFrames.Player:UpdateHealth()
    local hp = UnitHealth("player")
    local maxHp = UnitHealthMax("player")
    self.frame.health:SetMinMaxValues(0, maxHp)
    self.frame.health:SetValue(hp)
    
    if maxHp > 0 then
        self.frame.healthText:SetText(hp .. " / " .. maxHp)
    else
        self.frame.healthText:SetText(hp)
    end

    self:UpdateHealPrediction()
end

function SFrames.Player:UpdateHealPrediction()
    if not (self.frame and self.frame.health and self.frame.health.healPredMine and self.frame.health.healPredOther) then return end
    local predMine = self.frame.health.healPredMine
    local predOther = self.frame.health.healPredOther

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

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

    local _, mineIncoming, othersIncoming = GetIncomingHeals("player")
    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 = self.frame.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", self.frame.health, "TOPLEFT", currentWidth, 0)
        predMine:SetPoint("BOTTOMLEFT", self.frame.health, "BOTTOMLEFT", currentWidth, 0)
        predMine:SetWidth(mineWidth)
        predMine:Show()
    else
        predMine:Hide()
    end

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

function SFrames.Player:UpdatePowerType()
    local powerType = UnitPowerType("player")
    local color = SFrames.Config.colors.power[powerType]
    if color then
        self.frame.power:SetStatusBarColor(color.r, color.g, color.b)
    else
        self.frame.power:SetStatusBarColor(0, 0, 1)
    end
end

function SFrames.Player:GetDruidAltMana(currentPower, currentMaxPower)
    -- Method 1: DruidManaLib (tracks regen ticks, MP5, talents, shapeshift cost)
    if AceLibrary and AceLibrary.HasInstance and AceLibrary:HasInstance("DruidManaLib-1.0") then
        local ok, lib = pcall(function() return AceLibrary("DruidManaLib-1.0") end)
        if ok and lib and lib.GetMana then
            local mana, maxMana = lib:GetMana()
            if type(mana) == "number" and type(maxMana) == "number" and maxMana > 0 then
                return math.floor(mana + 0.5), math.floor(maxMana + 0.5)
            end
        end
    end

    -- Method 2: SuperWow returns real mana as second value of UnitMana
    if CheckSuperWow then
        local ok, hasSW = pcall(CheckSuperWow)
        if ok and hasSW then
            local ok2, _, realMana = pcall(UnitMana, "player")
            local ok3, _, realMax = pcall(UnitManaMax, "player")
            if ok2 and ok3 and type(realMana) == "number" and type(realMax) == "number" and realMax > 0 then
                return realMana, realMax
            end
        end
    end

    -- Method 3: TBC-style UnitPower API
    if UnitPower and UnitPowerMax then
        local okMana, pMana = pcall(function() return UnitPower("player", 0) end)
        local okMax, pMax = pcall(function() return UnitPowerMax("player", 0) end)
        if okMana and okMax and type(pMana) == "number" and type(pMax) == "number" and pMax > 0 then
            if not (pMax == currentMaxPower and pMana == currentPower) and pMax > 100 then
                return pMana, pMax
            end
        end
    end

    return nil, nil
end

function SFrames.Player:UpdateFiveSecondRule()
    if not (self.frame and self.frame.power and self.frame.power.fsrGlow) then return end

    local powerBar = self.frame.power
    local glow = powerBar.fsrGlow

    local function HideTicker()
        glow:Hide()
    end

    local powerType = UnitPowerType("player")
    if powerType ~= 0 then
        self.fiveSecondStart = nil
        self.fiveSecondPendingStart = nil
        self.fiveSecondLastMana = nil
        HideTicker()
        return
    end

    local now = GetTime()

    -- Continuous monitoring: whenever mana drops, (re)start 5-second rule.
    -- If mana drops while hard-casting, delay start until cast end.
    local currentMana = UnitMana("player") or 0
    if self.fiveSecondLastMana and currentMana < self.fiveSecondLastMana then
        local delay = 0
        local cb = self.frame and self.frame.castbar
        if cb and cb.casting and cb.startTime and cb.maxValue then
            local castEnd = cb.startTime + cb.maxValue
            if castEnd > now then
                delay = castEnd - now
            end
        end

        local startAt = now + delay + 0.08
        if delay > 0 then
            self.fiveSecondPendingStart = startAt
        else
            self.fiveSecondStart = startAt
            self.fiveSecondPendingStart = nil
        end
    end
    self.fiveSecondLastMana = currentMana

    if self.fiveSecondPendingStart then
        if now >= self.fiveSecondPendingStart then
            self.fiveSecondStart = self.fiveSecondPendingStart
            self.fiveSecondPendingStart = nil
        else
            HideTicker()
            return
        end
    end

    local startTime = self.fiveSecondStart
    if not startTime then
        HideTicker()
        return
    end

    local elapsed = now - startTime
    if elapsed < 0 then
        HideTicker()
        return
    end
    if elapsed >= 5 then
        self.fiveSecondStart = nil
        HideTicker()
        return
    end

    local barWidth = powerBar:GetWidth() or 0
    if barWidth <= 0 then
        HideTicker()
        return
    end

    local progress = elapsed / 5
    if progress < 0 then progress = 0 end
    if progress > 1 then progress = 1 end

    local glowWidth = 40
    if glowWidth > barWidth then glowWidth = barWidth end

    local maxGlowX = barWidth - glowWidth

    -- Move strictly from left edge to right edge across full 5 seconds.
    local glowX = math.floor(progress * maxGlowX + 0.5)

    local pulse = 0.82 + 0.18 * math.sin(GetTime() * 10)

    glow:ClearAllPoints()
    glow:SetPoint("TOPLEFT", powerBar, "TOPLEFT", glowX, 0)
    glow:SetPoint("BOTTOMLEFT", powerBar, "BOTTOMLEFT", glowX, 0)
    glow:SetWidth(glowWidth)
    glow:SetAlpha(0.62 * pulse)
    glow:Show()
end

function SFrames.Player:UpdatePower()
    local power = UnitMana("player")
    local maxPower = UnitManaMax("player")
    self.frame.power:SetMinMaxValues(0, maxPower)
    self.frame.power:SetValue(power)
    self.frame.powerText:SetText(power .. " / " .. maxPower)

    local _, class = UnitClass("player")
    local powerType = UnitPowerType("player")

    self:UpdateFiveSecondRule()

    if class ~= "DRUID" then
        if self.frame.manaText then self.frame.manaText:Hide() end
        if self.frame.manaBar then self.frame.manaBar:Hide() end
        return
    end

    if not self.druidManaCache then
        self.druidManaCache = { value = 0, max = 0, ts = GetTime() }
    end

    if powerType == 0 then
        self.druidManaCache.value = power or 0
        self.druidManaCache.max = maxPower or 0
        self.druidManaCache.ts = GetTime()
        if self.frame.manaText then self.frame.manaText:Hide() end
        if self.frame.manaBar then self.frame.manaBar:Hide() end
        return
    end

    if not self.frame.manaText then
        return
    end

    local mana, maxMana = self:GetDruidAltMana(power, maxPower)
    if mana and maxMana and maxMana > 0 then
        self.druidManaCache.value = mana
        self.druidManaCache.max = maxMana
        self.druidManaCache.ts = GetTime()
    else
        local cache = self.druidManaCache
        if cache and cache.max and cache.max > 0 then
            local now = GetTime()
            local dt = now - (cache.ts or now)
            if dt < 0 then dt = 0 end

            local regen = 0
            if GetManaRegen then
                local base, casting = GetManaRegen()
                if type(base) == "number" and base > 0 then
                    regen = base
                elseif type(casting) == "number" and casting > 0 then
                    regen = casting
                end
            end
            if (not regen or regen <= 0) and UnitStat then
                local ok, _, spi = pcall(UnitStat, "player", 5)
                if not (ok and type(spi) == "number" and spi > 0) then
                    ok, spi = pcall(function() return UnitStat("player", 5) end)
                end
                if ok and type(spi) == "number" and spi > 0 then
                    regen = (math.ceil(spi / 5) + 15) / 2
                end
            end
            if regen and regen > 0 and dt > 0 then
                cache.value = math.min(cache.max, (cache.value or 0) + regen * dt)
            end
            cache.ts = now
            mana = math.floor((cache.value or 0) + 0.5)
            maxMana = cache.max
        end
    end

    if maxMana and maxMana > 0 then
        local pct = math.floor(mana / maxMana * 100 + 0.5)
        self.frame.manaText:SetText(pct .. "%  " .. mana)
        self.frame.manaText:Show()
        if self.frame.manaBar then
            self.frame.manaBar:SetMinMaxValues(0, maxMana)
            self.frame.manaBar:SetValue(mana)
            self.frame.manaBar:Show()
        end
    else
        self.frame.manaText:SetText("--")
        self.frame.manaText:Show()
        if self.frame.manaBar then self.frame.manaBar:Hide() end
    end
end

--------------------------------------------------------------------------------
-- Player Auras (Buffs / Debuffs)
--------------------------------------------------------------------------------

function SFrames.Player:CreateAuras()
    -- Create 16 Buff Slots
    self.frame.buffs = {}
    self.frame.debuffs = {}
    local size = 24
    local spacing = 2
    local rowSpacing = 1
    local buffsPerRow = 9
    
    for i = 1, 16 do
        local b = CreateFrame("Button", "SFramesPlayerBuff"..i, self.frame)
        b:SetWidth(size)
        b:SetHeight(size)
        SFrames:CreateUnitBackdrop(b)
        
        b.icon = b:CreateTexture(nil, "ARTWORK")
        b.icon:SetPoint("TOPLEFT", b, "TOPLEFT", 1, -1)
        b.icon:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -1, 1)
        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)
        
        -- Tooltip support for precise buff checking
        b:SetScript("OnEnter", function()
            GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT")
            GameTooltip:SetPlayerBuff(this.buffIndex)
        end)
        b:SetScript("OnLeave", function() GameTooltip:Hide() end)
        
        -- Default row anchor
        if i == 1 then
            b:SetPoint("TOPLEFT", self.frame, "BOTTOMLEFT", 0, -1)
        elseif math.mod(i - 1, buffsPerRow) == 0 then
            b:SetPoint("TOP", self.frame.buffs[i-buffsPerRow], "BOTTOM", 0, -rowSpacing)
        else
            b:SetPoint("LEFT", self.frame.buffs[i-1], "RIGHT", spacing, 0)
        end
        
        b:Hide()
        self.frame.buffs[i] = b
    end
end

function SFrames.Player:UpdateAuras()
    local slotIdx = 0
    for i = 0, 31 do
        local buffIndex, untilCancelled = GetPlayerBuff(i, "HELPFUL")
        if buffIndex and buffIndex >= 0 then
            if not SFrames:IsBuffHidden(buffIndex) then
                slotIdx = slotIdx + 1
                if slotIdx > 16 then break end
                local b = self.frame.buffs[slotIdx]
                local texture = GetPlayerBuffTexture(buffIndex)
                if texture then
                    b.icon:SetTexture(texture)
                    b.buffIndex = buffIndex
                    b:Show()

                    local timeLeft = GetPlayerBuffTimeLeft(buffIndex)
                    if timeLeft and timeLeft > 0 and timeLeft < 9999 then
                        b.cdText:SetText(SFrames:FormatTime(timeLeft))
                    else
                        b.cdText:SetText("")
                    end
                else
                    b:Hide()
                end
            end
        end
    end
    for j = slotIdx + 1, 16 do
        self.frame.buffs[j]:Hide()
    end
end

-- Initialization Hook for Auras and Castbar
local origInit = SFrames.Player.Initialize
function SFrames.Player:Initialize()
    origInit(self)
    
    -- Setup Auras
    self:CreateAuras()
    self.auraUpdater = CreateFrame("Frame")
    self.auraUpdater.timer = 0
    self.auraUpdater:SetScript("OnUpdate", function()
        this.timer = this.timer + arg1
        if this.timer >= 0.2 then
            SFrames.Player:UpdateFiveSecondRule()
            SFrames.Player:UpdateAuras()
            SFrames.Player:UpdatePower()
            SFrames.Player:UpdateHealPrediction()
            this.timer = 0
        end
    end)
    
    -- Setup Castbar
    self:CreateCastbar()
    
    -- Hide default castbar
    if CastingBarFrame then
        CastingBarFrame:UnregisterAllEvents()
        CastingBarFrame:Hide()
    end

    -- Register mover
    if SFrames.Movers and SFrames.Movers.RegisterMover and self.frame then
        SFrames.Movers:RegisterMover("PlayerFrame", self.frame, "玩家",
            "CENTER", "UIParent", "CENTER", -200, -100)
    end
end

--------------------------------------------------------------------------------
-- Player Castbar
--------------------------------------------------------------------------------

function SFrames.Player:CreateCastbar()
    local cb = SFrames:CreateStatusBar(self.frame, "SFramesPlayerCastbar")
    cb:SetHeight(SFrames.Config.castbarHeight)
    cb:SetPoint("BOTTOMRIGHT", self.frame, "TOPRIGHT", 0, 6)
    cb:SetPoint("BOTTOMLEFT", self.frame.portrait, "TOPLEFT", SFrames.Config.castbarHeight + 6, 6)
    
    local cbbg = CreateFrame("Frame", nil, self.frame)
    cbbg:SetPoint("TOPLEFT", cb, "TOPLEFT", -1, 1)
    cbbg:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", 1, -1)
    cbbg:SetFrameLevel(cb:GetFrameLevel() - 1)
    SFrames:CreateUnitBackdrop(cbbg)
    
    cb.bg = cb:CreateTexture(nil, "BACKGROUND")
    cb.bg:SetAllPoints()
    cb.bg:SetTexture(SFrames:GetTexture())
    cb.bg:SetVertexColor(0.2, 0.2, 0.2, 1)
    cb:SetStatusBarColor(1, 0.7, 0)
    
    cb.text = SFrames:CreateFontString(cb, 10, "LEFT")
    cb.text:SetPoint("LEFT", cb, "LEFT", 4, 0)
    
    cb.time = SFrames:CreateFontString(cb, 10, "RIGHT")
    cb.time:SetPoint("RIGHT", cb, "RIGHT", -4, 0)
    
    cb.icon = cb:CreateTexture(nil, "ARTWORK")
    cb.icon:SetWidth(SFrames.Config.castbarHeight + 2)
    cb.icon:SetHeight(SFrames.Config.castbarHeight + 2)
    cb.icon:SetPoint("RIGHT", cb, "LEFT", -4, 0)
    cb.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
    
    -- Icon Backdrop
    local ibg = CreateFrame("Frame", nil, self.frame)
    ibg:SetPoint("TOPLEFT", cb.icon, "TOPLEFT", -1, 1)
    ibg:SetPoint("BOTTOMRIGHT", cb.icon, "BOTTOMRIGHT", 1, -1)
    ibg:SetFrameLevel(cb:GetFrameLevel() - 1)
    SFrames:CreateUnitBackdrop(ibg)
    
    cb:Hide()
    cbbg:Hide()
    cb.icon:Hide()
    ibg:Hide()
    
    self.frame.castbar = cb
    self.frame.castbar.cbbg = cbbg
    self.frame.castbar.ibg = ibg
    
    cb:SetScript("OnUpdate", function() SFrames.Player:CastbarOnUpdate() end)
    
    -- Hook events
    SFrames:RegisterEvent("SPELLCAST_START", function() self:CastbarStart(arg1, arg2) end)
    SFrames:RegisterEvent("SPELLCAST_STOP", function() self:CastbarStop() end)
    SFrames:RegisterEvent("SPELLCAST_FAILED", function() self:CastbarStop() end)
    SFrames:RegisterEvent("SPELLCAST_INTERRUPTED", function() self:CastbarStop() end)
    SFrames:RegisterEvent("SPELLCAST_DELAYED", function() self:CastbarDelayed(arg1) end)
    SFrames:RegisterEvent("SPELLCAST_CHANNEL_START", function() self:CastbarChannelStart(arg1, arg2) end)
    SFrames:RegisterEvent("SPELLCAST_CHANNEL_UPDATE", function() self:CastbarChannelUpdate(arg1) end)
    SFrames:RegisterEvent("SPELLCAST_CHANNEL_STOP", function() self:CastbarStop() end)
end

function SFrames.Player:CastbarStart(spellName, duration)
    local cb = self.frame.castbar
    cb.casting = true
    cb.channeling = nil
    cb.fadeOut = nil
    cb.startTime = GetTime()
    cb.maxValue = duration / 1000
    cb:SetMinMaxValues(0, cb.maxValue)
    cb:SetValue(0)
    cb.text:SetText(spellName)
    
    local texture
    local _UnitCastingInfo = UnitCastingInfo or (ShaguTweaks and ShaguTweaks.UnitCastingInfo)
    if _UnitCastingInfo then
        local _, _, _, tex = _UnitCastingInfo("player")
        texture = tex
    end
    if texture then
        cb.icon:SetTexture(texture)
        cb.icon:Show()
        cb.ibg:Show()
    else
        cb.icon:Hide()
        cb.ibg:Hide()
    end

    cb:SetAlpha(1)
    cb.cbbg:SetAlpha(1)
    if texture then
        cb.icon:SetAlpha(1)
        cb.ibg:SetAlpha(1)
    end
    cb:Show()
    cb.cbbg:Show()
end

function SFrames.Player:CastbarChannelStart(duration, spellName)
    local cb = self.frame.castbar
    cb.casting = nil
    cb.channeling = true
    cb.fadeOut = nil
    cb.startTime = GetTime()
    cb.maxValue = duration / 1000
    cb.endTime = cb.startTime + cb.maxValue
    cb:SetMinMaxValues(0, cb.maxValue)
    cb:SetValue(cb.maxValue)
    cb.text:SetText(spellName)
    
    local texture
    local _UnitChannelInfo = UnitChannelInfo or (ShaguTweaks and ShaguTweaks.UnitChannelInfo)
    if _UnitChannelInfo then
        local _, _, _, tex = _UnitChannelInfo("player")
        texture = tex
    end
    if texture then
        cb.icon:SetTexture(texture)
        cb.icon:Show()
        cb.ibg:Show()
    else
        cb.icon:Hide()
        cb.ibg:Hide()
    end

    cb:SetAlpha(1)
    cb.cbbg:SetAlpha(1)
    if texture then
        cb.icon:SetAlpha(1)
        cb.ibg:SetAlpha(1)
    end
    cb:Show()
    cb.cbbg:Show()
end

function SFrames.Player:CastbarStop()
    local cb = self.frame.castbar
    cb.casting = nil
    cb.channeling = nil
    cb.fadeOut = true
    -- keep showing for a short fade out
end

function SFrames.Player:CastbarDelayed(delay)
    local cb = self.frame.castbar
    if cb.casting then
        cb.maxValue = cb.maxValue + (delay / 1000)
        cb:SetMinMaxValues(0, cb.maxValue)
    end
end

function SFrames.Player:CastbarChannelUpdate(delay)
    local cb = self.frame.castbar
    if cb.channeling then
        local add = delay / 1000
        cb.maxValue = cb.maxValue + add
        cb.endTime = cb.endTime + add
        cb:SetMinMaxValues(0, cb.maxValue)
    end
end

function SFrames.Player:CastbarOnUpdate()
    local cb = self.frame.castbar
    if cb.casting then
        local elapsed = GetTime() - cb.startTime
        if elapsed >= cb.maxValue then
            cb.casting = nil
            cb.fadeOut = true
            cb:SetValue(cb.maxValue)
            return
        end
        cb:SetValue(elapsed)
        cb.time:SetText(string.format("%.1f", math.max(cb.maxValue - elapsed, 0)))
    elseif cb.channeling then
        local timeRemaining = cb.endTime - GetTime()
        if timeRemaining <= 0 then
            cb.channeling = nil
            cb.fadeOut = true
            cb:SetValue(0)
            return
        end
        cb:SetValue(timeRemaining)
        cb.time:SetText(string.format("%.1f", timeRemaining))
    elseif cb.fadeOut then
        local alpha = cb:GetAlpha() - 0.05
        if alpha > 0 then
            cb:SetAlpha(alpha)
            cb.cbbg:SetAlpha(alpha)
            cb.icon:SetAlpha(alpha)
            cb.ibg:SetAlpha(alpha)
        else
            cb.fadeOut = nil
            cb:Hide()
            cb.cbbg:Hide()
            cb.icon:Hide()
            cb.ibg:Hide()
        end
    end
end
