public void setSkinFromName(@NotNull Player target, @NotNull String skinName, boolean sendResponseMessage)
    {
        if (skinName.isEmpty() || !target.isOnline()) return;
        try {
            final PlayerProfile playerProfile = target.getPlayerProfile();
            playerProfile.setProperties(getTextures(skinName, 3));
            target.setPlayerProfile(playerProfile);

            if (sendResponseMessage) target.sendMessage("§aYour skin has been changed to: " + skinName);
        } catch (NullPointerException e) {
            if (sendResponseMessage) target.sendMessage("§cInvalid profile! ('" + skinName + "')");
        }
    }


    public static ItemStack playerHead(String name) throws ExecutionException, InterruptedException {
            ItemStack item = new ItemStack(Material.PLAYER_HEAD);
            SkullMeta meta = (SkullMeta) item.getItemMeta();
            PlayerProfile profile = Bukkit.createProfile(UUID.randomUUID(), name);
            CompletableFuture<Collection<ProfileProperty>> properties = SkinManager.cache.containsKey(name) ? (CompletableFuture<Collection<ProfileProperty>>) SkinManager.cache.get(name) : plugin.skinManager.getTexturesAsync(name, 3);
            if (properties != null) profile.setProperties(properties.get());
            meta.setPlayerProfile(profile);
            meta.setDisplayName("§r§a" + name.toLowerCase());
            List<String> lore = new ArrayList<>();
            lore.add("§r§f§lLEFT CLICK §r§ato apply skin§r");
            lore.add("§r§f§lRIGHT CLICK §r§cto remove skin§r");
            meta.setLore(lore);
            item.setItemMeta(meta);
            return item;
        }

        int maxSlots = 9 * 5 - 8;
                for (int i = 0; i < Math.min(favorites.size(), maxSlots); i++) {
                    menu.setItem(i + 9, SkinMorphItems.playerHead(favorites.get(i)));
                }



List<CompletableFuture<ItemStack>> futures = new ArrayList<>();
        for (int i = startIndex; i < endIndex; i++) {
            futures.add(SkinMorphItems.playerHead(favorites.get(i)));
        }

        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).thenRun(() -> {
            Bukkit.getScheduler().runTask(plugin, () -> {
                for (int i = 0; i < futures.size(); i++) {
                    final int slot = i + 9;
                    menu.setItem(slot, futures.get(i).join());
                }
                player.openInventory(menu);
            });
        });






/**
     * Get textures by UUID
     * @param _uuid The player's UUID
     * @param tries How many attempts the plugin should make to retrieve the textures
     * @return Collection<ProfileProperty> the textures
     */
    private Collection<ProfileProperty> getTextures(UUID _uuid, int tries)
    {
        final String uuid = _uuid.toString();
        if (cache.containsKey(uuid)) return cache.get(uuid);

        int retryDelay = 2000;

        for (int attempt = 1; attempt <= tries; attempt++) {
            try {
                final String skinResponse = makeRequest(SKIN_URL.formatted(uuid));
                if (skinResponse == null || skinResponse.startsWith("<!DOCTYPE")) {
                    if (plugin.DEBUG) plugin.getLogger().warning("Received invalid response (HTML or null) for skin data of '" + uuid + "'");
                    if (attempt < tries) {
                        Thread.sleep(retryDelay);
                        continue;
                    }
                    return null;
                }
                final JsonObject skinObject = JsonParser.parseString(skinResponse).getAsJsonObject().get("properties").getAsJsonArray().get(0).getAsJsonObject();
                final String value = skinObject.get("value").getAsString();
                final String signature = skinObject.get("signature").getAsString();
                final ProfileProperty profileProperty = new ProfileProperty("textures", value, signature);

                cache.put(uuid, List.of(profileProperty));

                if (plugin.DEBUG) plugin.getLogger().info("Successfully loaded textures for '" + uuid + "'!");
                return List.of(profileProperty);
            } catch (JsonSyntaxException | IllegalStateException e) {
                if (plugin.DEBUG) plugin.getLogger().warning("Failed to get textures for '" + uuid + "'! -> " + e.getMessage());
            } catch (Exception e) {
                if (plugin.DEBUG) plugin.getLogger().warning("Unexpected error while trying to get textures for '" + uuid + "'! -> " + e.getMessage());
            }
        }
        return null;
    }





if (args.length == 1) {
            completions.add("set"); // /skin set
            completions.add("reset"); // /skin reset
            completions.add("help"); // /skin help
            completions.add("favorites"); // /skin favorites add/remove
            completions.add("random"); // /skin random
        } else if (args.length == 2 && args[0].equalsIgnoreCase("set")) { // /skin set SELF
            if (sender instanceof final Player player) {
                for (Player onlinePlayer : Bukkit.getOnlinePlayers()) {
                    completions.add(onlinePlayer.getName());
                }
                List<String> favorites = plugin.favoritesManager.getFavorites(player);
                if (favorites != null && !favorites.isEmpty()) completions.addAll(favorites);
            }
        } else if (args.length == 2 && args[0].equalsIgnoreCase("random")) {
            for (Player onlinePlayer : Bukkit.getOnlinePlayers()) {
                completions.add(onlinePlayer.getName());
            }
        } else if (args.length == 2 && args[0].equalsIgnoreCase("favorites")) {
            completions.add("add");
            completions.add("remove");
        } else if (args.length == 3 && args[0].equalsIgnoreCase("favorites")) {
            if (args[1].equalsIgnoreCase("add")) {
                for (Player onlinePlayer : Bukkit.getOnlinePlayers()) completions.add(onlinePlayer.getName());
                if (sender instanceof final Player player) {
                    boolean containsCurrentSkin = completions.stream().anyMatch(s -> s.equalsIgnoreCase(plugin.skinManager.getCurrentSkin(player)));
                    if (!plugin.favoritesManager.isFavorite(player, plugin.skinManager.getCurrentSkin(player)) && !containsCurrentSkin) {
                        completions.add(plugin.skinManager.getCurrentSkin(player));
                    }
                }
            }
            if (args[1].equalsIgnoreCase("remove")) {
                if (sender instanceof final Player player) {
                    List<String> favorites = plugin.favoritesManager.getFavorites(player);
                    if (favorites != null && !favorites.isEmpty()) completions.addAll(favorites);
                }
            }
        } else if (args.length == 2 && args[0].equalsIgnoreCase("reset")) {
            for (Player onlinePlayer : Bukkit.getOnlinePlayers()) completions.add(onlinePlayer.getName());
        }






        //    public static CompletableFuture<ItemStack> favoriteSkinHead(Player player, String name)
        //    {
        //        return CompletableFuture.supplyAsync(() -> {
        //            ItemStack item = new ItemStack(Material.PLAYER_HEAD);
        //            SkullMeta meta = (SkullMeta) item.getItemMeta();
        //            PlayerProfile profile = Bukkit.createProfile(UUID.randomUUID(), name);
        //
        //            Collection<ProfileProperty> cachedProperties = SkinManager.cache.get(name);
        //
        //            CompletableFuture<Collection<ProfileProperty>> propertiesFuture =
        //                    cachedProperties != null
        //                            ? CompletableFuture.completedFuture(cachedProperties)
        //                            : plugin.skinManager.getTexturesAsync(name, 3);
        //
        //            return propertiesFuture.thenApply(properties -> {
        //                if (properties != null) profile.setProperties(properties);
        //                meta.setPlayerProfile(profile);
        //                meta.setDisplayName("§r§a" + name.toLowerCase());
        //                meta.setLore(List.of(
        //                        "§r§f§lLEFT CLICK §r§ato apply skin§r",
        //                        "§r§f§lRIGHT CLICK §r§cto remove skin§r"
        //                ));
        //                item.setItemMeta(meta);
        //                return item;
        //            });
        //        }).thenCompose(f -> f);
        //    }


        private boolean isValidUsage(@NotNull final String[] args)
            {
                final int argsLength = args.length;

                if (argsLength == 0) return true;
                if (argsLength > 3) return false;

                if (argsLength == 1) {
                    String sub = args[0];

                    boolean isSubValid =    sub.equalsIgnoreCase("reset")
                                         || sub.equalsIgnoreCase("random")
                                         || sub.equalsIgnoreCase("help");

                    return isSubValid;
                }

                if (argsLength == 2) {
                    String sub = args[0];
                    String subsub = args[1];

                    boolean isSubValid =    sub.equalsIgnoreCase("reset")
                                         || sub.equalsIgnoreCase("random")
                                         || sub.equalsIgnoreCase("help");

                    boolean isSubSubValid =

                    return
                }

                return false;
            }


[17:51:07 WARN]: Couldn't look up profile properties for 7d8194f5-70c5-4dd8-aa03-64ffd740e5b3
com.mojang.authlib.exceptions.MinecraftClientHttpException: Status: 429
        at com.mojang.authlib.minecraft.client.MinecraftClient.readInputStream(MinecraftClient.java:103) ~[authlib-6.0.58.jar:?]
        at com.mojang.authlib.minecraft.client.MinecraftClient.get(MinecraftClient.java:56) ~[authlib-6.0.58.jar:?]
        at com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService.fetchProfileUncached(YggdrasilMinecraftSessionService.java:201) ~[authlib-6.0.58.jar:?]
        at com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService.fetchProfile(YggdrasilMinecraftSessionService.java:171) ~[authlib-6.0.58.jar:?]
        at com.destroystokyo.paper.profile.PaperMinecraftSessionService.fetchProfile(PaperMinecraftSessionService.java:35) ~[paper-1.21.8.jar:1.21.8-21-ed31825]
        at org.bukkit.craftbukkit.profile.CraftPlayerProfile.getUpdatedProfile(CraftPlayerProfile.java:175) ~[paper-1.21.8.jar:1.21.8-21-ed31825]
        at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1768) ~[?:?]
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) ~[?:?]
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) ~[?:?]
        at java.base/java.lang.Thread.run(Thread.java:1570) ~[?:?]
[17:51:09 WARN]: Couldn't look up profile properties for db84ddaa-26d4-4eb8-bd0b-8b240056d93d
com.mojang.authlib.exceptions.MinecraftClientHttpException: Status: 429
        at com.mojang.authlib.minecraft.client.MinecraftClient.readInputStream(MinecraftClient.java:103) ~[authlib-6.0.58.jar:?]
        at com.mojang.authlib.minecraft.client.MinecraftClient.get(MinecraftClient.java:56) ~[authlib-6.0.58.jar:?]
        at com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService.fetchProfileUncached(YggdrasilMinecraftSessionService.java:201) ~[authlib-6.0.58.jar:?]
        at com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService.fetchProfile(YggdrasilMinecraftSessionService.java:171) ~[authlib-6.0.58.jar:?]
        at com.destroystokyo.paper.profile.PaperMinecraftSessionService.fetchProfile(PaperMinecraftSessionService.java:35) ~[paper-1.21.8.jar:1.21.8-21-ed31825]
        at org.bukkit.craftbukkit.profile.CraftPlayerProfile.getUpdatedProfile(CraftPlayerProfile.java:175) ~[paper-1.21.8.jar:1.21.8-21-ed31825]
        at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1768) ~[?:?]
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) ~[?:?]
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) ~[?:?]
        at java.base/java.lang.Thread.run(Thread.java:1570) ~[?:?]
[17:51:09 WARN]: Couldn't look up profile properties for 95f3bdab-2c86-470e-8a5e-1f13fc5d0bd8
com.mojang.authlib.exceptions.MinecraftClientHttpException: Status: 429
        at com.mojang.authlib.minecraft.client.MinecraftClient.readInputStream(MinecraftClient.java:103) ~[authlib-6.0.58.jar:?]
        at com.mojang.authlib.minecraft.client.MinecraftClient.get(MinecraftClient.java:56) ~[authlib-6.0.58.jar:?]
        at com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService.fetchProfileUncached(YggdrasilMinecraftSessionService.java:201) ~[authlib-6.0.58.jar:?]
        at com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService.fetchProfile(YggdrasilMinecraftSessionService.java:171) ~[authlib-6.0.58.jar:?]
        at com.destroystokyo.paper.profile.PaperMinecraftSessionService.fetchProfile(PaperMinecraftSessionService.java:35) ~[paper-1.21.8.jar:1.21.8-21-ed31825]
        at org.bukkit.craftbukkit.profile.CraftPlayerProfile.getUpdatedProfile(CraftPlayerProfile.java:175) ~[paper-1.21.8.jar:1.21.8-21-ed31825]
        at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1768) ~[?:?]
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) ~[?:?]
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) ~[?:?]
        at java.base/java.lang.Thread.run(Thread.java:1570) ~[?:?]




        List<String> favorites = favoritesManager.getFavorites(player);
                if (favorites == null) favorites = new ArrayList<>();



                String chosenFavorite = loadedFavorites.get(ThreadLocalRandom.current().nextInt(loadedFavorites.size()));
                        boolean alreadyWearingChosenFavorite = skinManager.wearsSkin(player, chosenFavorite);
                        return alreadyWearingChosenFavorite ? loadedFavorites.get(ThreadLocalRandom.current().nextInt(loadedFavorites.size()) - 1) : chosenFavorite;










                        package com.luvoos.skinmorph.command;

                        import com.luvoos.skinmorph.SkinMorph;
                        import org.bukkit.Bukkit;
                        import org.bukkit.command.Command;
                        import org.bukkit.command.CommandSender;
                        import org.bukkit.command.TabCompleter;
                        import org.bukkit.entity.Player;
                        import org.jetbrains.annotations.NotNull;
                        import org.jetbrains.annotations.Nullable;

                        import java.util.ArrayList;
                        import java.util.List;

                        public class SkinMorphTabCompleter implements TabCompleter {

                            private final SkinMorph plugin;

                            public SkinMorphTabCompleter(SkinMorph plugin)
                            {
                                this.plugin = plugin;
                            }

                            @Override
                            public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String @NotNull [] args)
                            {
                                List<String> completions = new ArrayList<>();

                                if (args.length == 1) {
                                    completions.add("set"); // /skin set
                                    completions.add("reset"); // /skin reset
                                    completions.add("help"); // /skin help
                                    completions.add("favorites"); // /skin favorites
                                    completions.add("random"); // /skin random
                                    completions.add("settings"); // /skin settings
                                } else if (args.length == 2 && args[0].equalsIgnoreCase("set")) { // /skin set SELF
                                    if (sender instanceof final Player player) {
                                        for (Player onlinePlayer : Bukkit.getOnlinePlayers()) {
                                            completions.add(onlinePlayer.getName());
                                        }
                                        List<String> favorites = plugin.favoritesManager.getFavorites(player);
                                        if (favorites != null && !favorites.isEmpty()) completions.addAll(favorites);
                                    }
                                } else if (args.length == 3 && args[0].equalsIgnoreCase("set")) { // /skin set OTHER
                                    if (sender instanceof final Player player) {
                                        for (Player onlinePlayer : Bukkit.getOnlinePlayers()) {
                                            completions.add(onlinePlayer.getName());
                                        }
                                    }
                                } else if (args.length == 2 && args[0].equalsIgnoreCase("random")) {
                                    for (Player onlinePlayer : Bukkit.getOnlinePlayers()) {
                                        completions.add(onlinePlayer.getName());
                                    }
                                } else if (args.length == 2 && args[0].equalsIgnoreCase("favorites")) {
                                    completions.add("add");
                                    completions.add("remove");
                                } else if (args.length == 3 && args[0].equalsIgnoreCase("favorites")) {
                                    if (args[1].equalsIgnoreCase("add")) {
                                        for (Player onlinePlayer : Bukkit.getOnlinePlayers()) completions.add(onlinePlayer.getName());
                                        if (sender instanceof final Player player) {
                                            boolean containsCurrentSkin = completions.stream().anyMatch(s -> s.equalsIgnoreCase(plugin.skinManager.getCurrentSkin(player)));
                                            if (!plugin.favoritesManager.isFavorite(player, plugin.skinManager.getCurrentSkin(player)) && !containsCurrentSkin) {
                                                completions.add(plugin.skinManager.getCurrentSkin(player));
                                            }
                                        }
                                    }
                                    if (args[1].equalsIgnoreCase("remove")) {
                                        if (sender instanceof final Player player) {
                                            List<String> favorites = plugin.favoritesManager.getFavorites(player);
                                            if (favorites != null && !favorites.isEmpty()) completions.addAll(favorites);
                                        }
                                    }
                                } else if (args.length == 2 && args[0].equalsIgnoreCase("reset")) {
                                    for (Player onlinePlayer : Bukkit.getOnlinePlayers()) completions.add(onlinePlayer.getName());
                                }

                                return completions;
                            }
                        }


                .filter(name -> exclude == null || !name.equalsIgnoreCase(exclude.getName()))









                if (hasRandomSkinItem) menu.setItem(hasResetSkinItem ? 2 : 3, SkinMorphItems.randomSkin(player));
                        if (hasResetSkinItem) menu.setItem(3, SkinMorphItems.resetSkin(player));
                        menu.setItem(4, SkinMorphItems.changeSkin(player));
                        menu.setItem(5, SkinMorphItems.manageFavorites());
                        menu.setItem(8, SkinMorphItems.settings());







package com.luvoos.skinmorph;

import com.destroystokyo.paper.profile.PlayerProfile;
import com.destroystokyo.paper.profile.ProfileProperty;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import com.luvoos.skinmorph.util.FormatUtil;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;

public class SkinManager
{
    private final SkinMorph plugin = SkinMorph.instance;
    public static final String PROFILE_URL = "https://api.mojang.com/users/profiles/minecraft/";
    public static final String SKIN_URL = "https://sessionserver.mojang.com/session/minecraft/profile/%s?unsigned=false";
    public static final HttpClient HTTP_CLIENT = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).connectTimeout(Duration.ofSeconds(10)).build();
    public static final Map<UUID, String> currentSkins = new ConcurrentHashMap<>();

    public void setSkinFromName(@NotNull Player target, @NotNull String skinName, boolean playerFeedback)
    {
        setSkinFromName(target, skinName, playerFeedback, 3);
    }

    public void setSkinFromName(@NotNull Player target, @NotNull String skinName, boolean playerFeedback, int tries)
    {
        if (skinName.isEmpty() || !target.isOnline()) return;

        String normalizedSkinName = FormatUtil.normalize(skinName);

        if (isWearingSkin(target, skinName)) {
            if (plugin.PLAYER_FEEDBACK && playerFeedback) target.sendMessage("§aYou're already wearing that skin! ('§e" + normalizedSkinName + "§a')");
            return;
        }

        Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
            try {
                final PlayerProfile profile = target.getPlayerProfile();
                final SkinData skin = getSkin(skinName, 3);
                if (skin == null) {
                    if (plugin.PLAYER_FEEDBACK && playerFeedback) target.sendMessage("§cInvalid profile! ('§e" + normalizedSkinName + "§c')");
                    return;
                }
                final Collection<ProfileProperty> textures = skin.getTextureProperties();

                profile.setProperties(textures);

                Bukkit.getScheduler().runTask(plugin, () -> {
                    target.setPlayerProfile(profile);
                    setCurrentSkin(target, normalizedSkinName);
                    plugin.selectedSkinsManager.setSelectedSkin(target, skinName);
                    if (plugin.PLAYER_FEEDBACK && playerFeedback) target.sendMessage("§aYour skin has been changed to §e" + normalizedSkinName + "§a!");
                });

            } catch (Exception e) {
                if (plugin.PLAYER_FEEDBACK && playerFeedback) target.sendMessage("§cError changing skin: " + e.getMessage());
            }
        });
    }

    public void setSkinFromName(@NotNull CommandSender caller, @NotNull Player target, @NotNull String skinName, boolean playerFeedback)
    {
        setSkinFromName(caller, target, skinName, playerFeedback, 3);
    }

    public void setSkinFromName(@NotNull CommandSender caller, @NotNull Player target, @NotNull String skinName, boolean playerFeedback, int tries)
    {
        if (skinName.isEmpty() || !target.isOnline()) return;

        String normalizedSkinName = FormatUtil.normalize(skinName);

        if (isWearingSkin(target, skinName)) {
            if (plugin.PLAYER_FEEDBACK && playerFeedback) caller.sendMessage("§a" + target.getName() + " is already wearing that skin! ('§e" + normalizedSkinName + "§a')");
            return;
        }

        Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
            try {
                final PlayerProfile profile = target.getPlayerProfile();
                final SkinData skin = getSkin(skinName, 3);
                if (skin == null) {
                    if (plugin.PLAYER_FEEDBACK && playerFeedback) target.sendMessage("§cInvalid profile! ('§e" + normalizedSkinName + "§c')");
                    return;
                }
                final Collection<ProfileProperty> textures = skin.getTextureProperties();

                profile.setProperties(textures);

                Bukkit.getScheduler().runTask(plugin, () -> {
                    target.setPlayerProfile(profile);
                    setCurrentSkin(target, normalizedSkinName);
                    plugin.selectedSkinsManager.setSelectedSkin(target, skinName);
                    if (plugin.PLAYER_FEEDBACK && playerFeedback) target.sendMessage("§aYour skin has been changed to §e" + normalizedSkinName + "§a!");
                    boolean endsWithS = target.getName().endsWith("s") || target.getName().endsWith("S");
                    if (plugin.PLAYER_FEEDBACK && playerFeedback) caller.sendMessage("§a" + target.getName() + (endsWithS ? "'" : "'s") + " skin has been changed to §e" + normalizedSkinName + "§a!");
                });

            } catch (Exception e) {
                if (plugin.PLAYER_FEEDBACK && playerFeedback) caller.sendMessage("§cError changing skin: " + e.getMessage());
            }
        });
    }

    public void setSkinFromName(@NotNull Player caller, @NotNull Player target, @NotNull String skinName, boolean playerFeedback)
    {
        setSkinFromName(caller, target, skinName, playerFeedback, 3);
    }

    public void setSkinFromName(@NotNull Player caller, @NotNull Player target, @NotNull String skinName, boolean playerFeedback, int tries)
    {
        if (skinName.isEmpty() || !target.isOnline()) return;

        String normalizedSkinName = FormatUtil.normalize(skinName);

        if (isWearingSkin(target, skinName)) {
            if (plugin.PLAYER_FEEDBACK && playerFeedback) caller.sendMessage("§a" + target.getName() + " is already wearing that skin! ('§e" + normalizedSkinName + "§a')");
            return;
        }

        Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
            try {
                final PlayerProfile profile = target.getPlayerProfile();
                final SkinData skin = getSkin(skinName, 3);
                if (skin == null) {
                    if (plugin.PLAYER_FEEDBACK && playerFeedback) target.sendMessage("§cInvalid profile! ('§e" + normalizedSkinName + "§c')");
                    return;
                }
                final Collection<ProfileProperty> textures = skin.getTextureProperties();

                profile.setProperties(textures);

                Bukkit.getScheduler().runTask(plugin, () -> {
                    target.setPlayerProfile(profile);
                    setCurrentSkin(target, normalizedSkinName);
                    plugin.selectedSkinsManager.setSelectedSkin(target, skinName);
                    if (plugin.PLAYER_FEEDBACK && playerFeedback) target.sendMessage("§aYour skin has been changed to §e" + normalizedSkinName + "§a!");
                    boolean endsWithS = target.getName().endsWith("s") || target.getName().endsWith("S");
                    if (plugin.PLAYER_FEEDBACK && playerFeedback) caller.sendMessage("§a" + target.getName() + (endsWithS ? "'" : "'s") + " skin has been changed to §e" + normalizedSkinName + "§a!");
                });

            } catch (Exception e) {
                if (plugin.PLAYER_FEEDBACK && playerFeedback) caller.sendMessage("§cError changing skin: " + e.getMessage());
            }
        });
    }

    public void resetSkin(@NotNull Player target, boolean playerFeedback)
    {
        target.closeInventory();
        if (isWearingSkin(target, target.getName().toLowerCase())) {
            if (plugin.PLAYER_FEEDBACK && playerFeedback) target.sendMessage("§aYou're already wearing your §edefault skin§a!");
            return;
        }
        setSkinFromName(target, target.getName().toLowerCase(), false);
        if (plugin.PLAYER_FEEDBACK && playerFeedback) target.sendMessage("§aYour skin has been reset to your §edefault skin§a!");
    }

    public void resetSkin(@NotNull CommandSender caller, @NotNull Player target, boolean playerFeedback)
    {
        if (isWearingSkin(target, target.getName().toLowerCase())) {
            if (plugin.PLAYER_FEEDBACK && playerFeedback) caller.sendMessage("§a" + target.getName() + " is already wearing their §edefault skin§a!");
            return;
        }
        setSkinFromName(target, target.getName().toLowerCase(), false);
        if (plugin.PLAYER_FEEDBACK && playerFeedback) target.sendMessage("§aYour skin has been reset to your §edefault skin§a!");
        boolean endsWithS = target.getName().endsWith("s") || target.getName().endsWith("S");
        if (plugin.PLAYER_FEEDBACK && playerFeedback) caller.sendMessage("§a" + target.getName() + (endsWithS ? "'" : "'s") + " skin has been reset to their §edefault skin§a!");
    }

    public void resetSkin(@NotNull Player caller, @NotNull Player target, boolean playerFeedback)
    {
        if (isWearingSkin(target, target.getName().toLowerCase())) {
            if (plugin.PLAYER_FEEDBACK && playerFeedback) caller.sendMessage("§a" + target.getName() + " is already wearing their §edefault skin§a!");
            return;
        }
        setSkinFromName(target, target.getName().toLowerCase(), false);
        if (plugin.PLAYER_FEEDBACK && playerFeedback) target.sendMessage("§aYour skin has been reset to your §edefault skin§a!");
        boolean endsWithS = target.getName().endsWith("s") || target.getName().endsWith("S");
        if (plugin.PLAYER_FEEDBACK && playerFeedback) caller.sendMessage("§a" + target.getName() + (endsWithS ? "'" : "'s") + " skin has been reset to their §edefault skin§a!");
    }

    public void applyRandomSkin(@NotNull Player target, boolean playerFeedback)
    {
        String randomSkin = plugin.favoritesManager.getRandomFavorite(target);
        if (randomSkin.isEmpty()) {
            if (plugin.PLAYER_FEEDBACK && playerFeedback) target.sendMessage("§cYou need to have at least §e2 §cfavorites to do that!");
            return;
        }
        setSkinFromName(target, randomSkin, false);
        if (plugin.PLAYER_FEEDBACK && playerFeedback) target.sendMessage("§aRandomly picked... §e" + randomSkin.toLowerCase() + "§a!");
    }

    public void applyRandomSkin(@NotNull CommandSender caller, @NotNull Player target, boolean playerFeedback)
    {
        String randomSkin = plugin.favoritesManager.getRandomFavorite(target);
        if (randomSkin.isEmpty()) {
            if (plugin.PLAYER_FEEDBACK && playerFeedback) caller.sendMessage("§c" + target.getName() + " needs to have at least §e2 §cfavorites to do that!");
            return;
        }
        setSkinFromName(target, randomSkin, false);
        if (plugin.PLAYER_FEEDBACK && playerFeedback) target.sendMessage("§aRandomly picked... §e" + randomSkin.toLowerCase() + "§a!");
        if (plugin.PLAYER_FEEDBACK && playerFeedback) caller.sendMessage("§aRandomly picked... §e" + randomSkin.toLowerCase() + "§a!");
    }

    public void applyRandomSkin(@NotNull Player caller, @NotNull Player target, boolean playerFeedback)
    {
        String randomSkin = plugin.favoritesManager.getRandomFavorite(target);
        if (randomSkin.isEmpty()) {
            if (plugin.PLAYER_FEEDBACK && playerFeedback) caller.sendMessage("§c" + target.getName() + " needs to have at least §e2 §cfavorites to do that!");
            return;
        }
        setSkinFromName(target, randomSkin, false);
        if (plugin.PLAYER_FEEDBACK && playerFeedback) target.sendMessage("§aRandomly picked... §e" + randomSkin.toLowerCase() + "§a!");
        if (plugin.PLAYER_FEEDBACK && playerFeedback) caller.sendMessage("§aRandomly picked... §e" + randomSkin.toLowerCase() + "§a!");
    }

    /**
     * Get SkinData containing textures by username asynchronously.
     * @param skinName Minecraft username
     * @param attempts amount of attempts
     * @return SkinData containing the textures
     */
    public CompletableFuture<SkinData> getSkinAsync(String skinName, int attempts)
    {
        return CompletableFuture.supplyAsync(() -> getSkin(skinName, attempts));
    }

    /**
     * Get SkinData containing textures by username.
     * @param skinName Minecraft username
     * @param attempts amount of attempts
     * @return SkinData containing the textures
     */
    private SkinData getSkin(String skinName, int attempts)
    {
        skinName = FormatUtil.normalize(skinName);
        //if (cache.containsKey(skinName)) return cache.get(skinName);

        final SkinData pCached = plugin.cacheManager.getSkin(skinName);
        if (pCached != null) return pCached;

        if (skinName.length() > 16) {
            plugin.getLogger().warning("'" + skinName + "' exceeds the maximum username length (16)!");
            return null;
        }

        int delay = plugin.RETRY_DELAY;

        for (int attempt = 1; attempt <= attempts; attempt++) {
            try {
                final String profileResponse = makeRequest(PROFILE_URL + skinName);
                if (profileResponse == null || profileResponse.startsWith("<!DOCTYPE")) {
                    if (plugin.DEBUG) plugin.getLogger().warning("Invalid profile response for '" + skinName + "'");
                    delay(attempt, attempts, delay);
                    continue;
                }

                JsonObject profileObject = JsonParser.parseString(profileResponse).getAsJsonObject();
                if (!profileObject.has("id")) {
                    if (plugin.DEBUG) plugin.getLogger().warning("Missing 'id' field in profile response for '" + skinName + "'");
                    delay(attempt, attempts, delay);
                    continue;
                }

                final String uuid = profileObject.get("id").getAsString();
                final String skinResponse = makeRequest(String.format(SKIN_URL, uuid));

                if (skinResponse == null || skinResponse.startsWith("<!DOCTYPE")) {
                    if (plugin.DEBUG) plugin.getLogger().warning("Invalid skin response for '" + skinName + "'");
                    delay(attempt, attempts, delay);
                    continue;
                }

                JsonObject skinObject = JsonParser.parseString(skinResponse)
                        .getAsJsonObject()
                        .get("properties")
                        .getAsJsonArray()
                        .get(0)
                        .getAsJsonObject();

                final ProfileProperty profileProperty = new ProfileProperty(
                        "textures",
                        skinObject.get("value").getAsString(),
                        skinObject.get("signature").getAsString()
                );

                final SkinData skinData = new SkinData(skinName, List.of(profileProperty), System.currentTimeMillis());

                plugin.cacheManager.updateCache(skinData);
                if (plugin.DEBUG) plugin.getLogger().info("Successfully loaded textures for '" + skinName + "'!");

                return skinData;
            } catch (JsonSyntaxException | IllegalStateException e) {
                if (plugin.DEBUG) plugin.getLogger().warning("Failed to parse JSON for '" + skinName + "'");
            } catch (Exception e) {
                if (plugin.DEBUG) plugin.getLogger().warning("Unexpected error while fetching '" + skinName + "' -> " + e.getMessage());
            }
        }

        return null;
    }

    /**
     * Performs an HTTP request and returns its response body.
     * @param url
     * @return response body
     */
    public String makeRequest(String url)
    {
        try {
            final HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).build();
            final HttpResponse<String> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
            int status = response.statusCode();
            if (plugin.DEBUG) {
                if (status == 200) plugin.getLogger().info("Request " + url + " -> " + response.statusCode());
                if (status == 404) plugin.getLogger().warning("Request " + url + " -> " + response.statusCode());
            }
            return response.body();
        } catch (IOException | InterruptedException e) {
            if (plugin.DEBUG) plugin.getLogger().warning("Request failed for " + url + " -> " + e.getMessage());
            return null;
        }
    }

    public void setCurrentSkin(@NotNull final Player player, @NotNull String skinName)
    {
        final UUID uuid = player.getUniqueId();
        final String noSpacesSkinName = skinName.replace(" ", "");
        currentSkins.put(uuid, noSpacesSkinName.toLowerCase());
    }

    public String getCurrentSkin(@NotNull final Player player)
    {
        final UUID uuid = player.getUniqueId();
        return currentSkins.get(uuid);
    }

    public boolean isWearingSkin(@NotNull final Player player, @NotNull String skinName)
    {
        final UUID uuid = player.getUniqueId();
        final String noSpacesSkinName = skinName.replace(" ", "");
        return currentSkins.containsKey(uuid) && currentSkins.get(uuid).equalsIgnoreCase(noSpacesSkinName);
    }

    private void delay(int attempt, int attempts, int retryDelay)
    {
        if (attempt < attempts) {
            try {
                Thread.sleep(retryDelay);
            } catch (InterruptedException ignored) {}
        }
    }

    public void clearCache()
    {
        CacheManager.cache.clear();
    }
}







????//
            PlayerProfile profile = player.getPlayerProfile();
            profile.getTextures().setSkin(new URL("http://textures.minecraft.net/texture/c08845875e354a3b8d35c82553555cbe"));
            player.setPlayerProfile(profile);
            player.hidePlayer(plugin, player);
            player.showPlayer(plugin, player);
            //


???????