package de.mviper.spigot;

import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.event.inventory.PrepareAnvilEvent;
import org.bukkit.inventory.AnvilInventory;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.boss.BarColor;
import org.bukkit.boss.BarStyle;
import org.bukkit.boss.BossBar;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitTask;

import net.milkbowl.vault.economy.Economy;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.time.LocalDate;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

public class IngameShopSpigot extends JavaPlugin implements Listener {

    private static Economy econ = null;
    private String wpBase;
    private String wpUrlPending;
    private String wpUrlPendingOffline;
    private String wpUrlExecute;
    private String wpUrlComplete;
    private String wpUrlCancel;
    private String wpUrlSellItems;
    private String wpUrlSell;

    private Gson    gson  = new Gson();
    private boolean debug = false;
    private BukkitTask task;

    private String  currency          = "Coins";
    private String  targetServer      = "survival";
    private String  apiKey            = "";
    private boolean flyRedeemDisabled = false;
    private String  incomeReceiver    = "";
    private int     flyAboMaxDailySec = 21600; // 6 Stunden Standardlimit

    // Sell-Feature
    private double  sellPriceOffset   = 0.0;   // z.B. -10.0 = -10 % vom WordPress-Ankaufspreis
    private boolean sellEnabled       = true;

    private final Map<UUID, List<SellManager.SellEntry>> sellCache      = new HashMap<>();
    private final Map<UUID, Integer>                     sellAmountPage = new HashMap<>();
    private static final String GUI_SELL_TITLE = ChatColor.GOLD + "💰 Items verkaufen";

    private Map<Integer, OrderData> orderCache     = new HashMap<>();
    private Map<UUID, Integer>      activeOrderIds = new ConcurrentHashMap<>();

    private FlyManager       flyManager;
    private FlyCodeManager   flyCodeManager;
    private RankManager      rankManager;
    private FlyAboManager    flyAboManager;
    private PlotSlotManager  plotSlotManager;

    // Plot-Slot Konfiguration
    private String              plotServer       = "citybuild";
    private Map<String, Integer> plotRankDefaults = new HashMap<>();

    private static final String GUI_FLYCODES = ChatColor.GOLD + "✈ Deine Fly-Gutscheine";
    private final Map<UUID, Integer>                            flyCodesPage  = new HashMap<>();
    private final Map<UUID, List<FlyCodeManager.FlyCodeEntry>> flyCodesCache = new HashMap<>();
    private final Map<UUID, String> pendingTransferAnvil = new HashMap<>();
    private final Map<UUID, String> anvilTypedText       = new HashMap<>();

    // ===========================================================
    // LIFECYCLE
    // ===========================================================

    @Override
    public void onEnable() {
        if (!setupEconomy()) {
            getLogger().severe("Kein Economy-Plugin (Vault) gefunden! Shop deaktiviert.");
            getServer().getPluginManager().disablePlugin(this);
            return;
        }

        saveDefaultConfig();
        reloadConfig();

        String domain = getConfig().getString("wordpress-url", "http://localhost");
        if (domain.endsWith("/")) domain = domain.substring(0, domain.length() - 1);
        wpBase = domain + "/wp-json/wis/v1";

        wpUrlPending        = wpBase + "/pending_orders";
        wpUrlPendingOffline = wpBase + "/pending_offline";
        wpUrlExecute        = wpBase + "/execute_order";
        wpUrlComplete       = wpBase + "/complete_order";
        wpUrlCancel         = wpBase + "/cancel_order";
        wpUrlSellItems      = wpBase + "/sell_items";
        wpUrlSell           = wpBase + "/sell_item";

        targetServer      = getConfig().getString("server-name",       "survival").toLowerCase();
        currency          = getConfig().getString("currency-name",     "Coins");
        apiKey            = getConfig().getString("api-key",           "");
        debug             = getConfig().getBoolean("debug-mode",       false);
        flyRedeemDisabled = getConfig().getBoolean("fly-redeem-disabled", false);
        incomeReceiver    = getConfig().getString("income-receiver",   "");
        sellEnabled       = getConfig().getBoolean("sell.enabled", true);
        sellPriceOffset   = getConfig().getDouble("sell.price-offset", 0.0);
        flyAboMaxDailySec = getConfig().getInt("fly-abo.max-daily-hours", 6) * 3600;

        // Plot-Slot Konfiguration laden
        plotServer = getConfig().getString("plot-slots.server", "citybuild").toLowerCase();
        plotRankDefaults.clear();
        org.bukkit.configuration.ConfigurationSection rankSec =
            getConfig().getConfigurationSection("plot-slots.rank-defaults");
        if (rankSec != null) {
            for (String key : rankSec.getKeys(false))
                plotRankDefaults.put(key.toLowerCase(), rankSec.getInt(key, 2));
        } else {
            // Viper Network Defaults
            plotRankDefaults.put("member", 2);
            plotRankDefaults.put("vip",    3);
            plotRankDefaults.put("scout",  4);
            plotRankDefaults.put("primo",  5);
        }

        if (apiKey.isEmpty()) {
            getLogger().warning("⚠️  Kein api-key in config.yml gesetzt!");
        }

        flyCodeManager = new FlyCodeManager(this);
        if (!flyCodeManager.connect()) {
            getLogger().severe("❌ MySQL-Verbindung fehlgeschlagen! Plugin deaktiviert.");
            getServer().getPluginManager().disablePlugin(this);
            return;
        }
        flyCodeManager.createTable();
        flyManager  = new FlyManager(this);
        rankManager = new RankManager(this, flyCodeManager);
        rankManager.startExpiryChecker();
        flyAboManager = new FlyAboManager(this, flyCodeManager);
        flyAboManager.startDailyResetChecker();
        plotSlotManager = new PlotSlotManager(this, flyCodeManager);
        plotSlotManager.startBillingChecker();

        int  intervalSeconds = getConfig().getInt("check-interval", 10);
        long pollInterval    = intervalSeconds * 20L;

        getServer().getPluginManager().registerEvents(this, this);
        getCommand("orders").setExecutor(new OrdersCommand());
        getCommand("flytime").setExecutor(new FlyTimeCommand());
        getCommand("flyredeem").setExecutor(new FlyRedeemCommand());
        getCommand("flycodes").setExecutor(new FlyCodesCommand());
        getCommand("flygive").setExecutor(new FlyGiveCommand());
        getCommand("flypause").setExecutor(new FlyPauseCommand());
        getCommand("rankinfo").setExecutor(new RankInfoCommand());
        getCommand("flyabo").setExecutor(new FlyAboCommand());
        getCommand("flyabocancel").setExecutor(new FlyAboCancelCommand());
        getCommand("flyabogive").setExecutor(new FlyAboGiveCommand());
        getCommand("plotslots").setExecutor(new PlotSlotsCommand());
        getCommand("plotabo").setExecutor(new PlotAboCommand());
        getCommand("plotabocancel").setExecutor(new PlotAboCancelCommand());
        getCommand("plotabogive").setExecutor(new PlotAboGiveCommand());
        getCommand("plotslotsgive").setExecutor(new PlotSlotsGiveCommand());
        getCommand("abo").setExecutor(new AboOverviewCommand());
        getCommand("sell").setExecutor(new SellCommand());
        getCommand("wpis").setExecutor(new ReloadCommand());

        startPolling(pollInterval);
        flyManager.startSessionPersist();
        startSellItemPolling();

        getLogger().info("IngameShopSpigot v6.4 aktiv.");
    }

    @Override
    public void onDisable() {
        if (task           != null) task.cancel();
        if (flyManager     != null) flyManager.cancelAll();
        if (flyCodeManager != null) flyCodeManager.disconnect();
        getLogger().info("IngameShopSpigot gestoppt.");
    }

    private boolean setupEconomy() {
        RegisteredServiceProvider<Economy> rsp =
            getServer().getServicesManager().getRegistration(Economy.class);
        if (rsp == null) return false;
        econ = rsp.getProvider();
        return econ != null;
    }

    // ===========================================================
    // POLLING
    // ===========================================================

    /** Lädt die Ankaufliste initial und alle 5 Minuten neu aus WordPress */
    private void startSellItemPolling() {
        Runnable refresh = () -> new BukkitRunnable() {
            @Override public void run() {
                try {
                    HttpURLConnection conn = openAuthConnection(
                        wpUrlSellItems + "?server=" + targetServer, "GET");
                    if (conn.getResponseCode() == 200) {
                        String     body  = readResponse(conn);
                        JsonObject json  = JsonParser.parseString(body).getAsJsonObject();
                        JsonArray  items = json.getAsJsonArray("items");
                        List<SellManager.SellEntry> list = new ArrayList<>();
                        if (items != null) {
                            for (JsonElement el : items) {
                                JsonObject o = el.getAsJsonObject();
                                list.add(new SellManager.SellEntry(
                                    o.get("item_id").getAsString(),
                                    o.get("name").getAsString(),
                                    o.get("buy_price").getAsDouble(),
                                    o.get("sell_price").getAsDouble()
                                ));
                            }
                        }
                        SellManager.getInstance().load(list);
                        if (debug) getLogger().info("[Sell] " + list.size() + " Items geladen.");
                    }
                } catch (Exception e) {
                    if (debug) getLogger().log(Level.WARNING, "[Sell] Ladefehler", e);
                }
            }
        }.runTaskAsynchronously(this);

        refresh.run();
        // Alle 5 Minuten neu laden
        new BukkitRunnable() {
            @Override public void run() { refresh.run(); }
        }.runTaskTimer(this, 20L * 300, 20L * 300);
    }

    private void startPolling(long intervalTicks) {
        this.task = new BukkitRunnable() {
            @Override public void run() {
                new BukkitRunnable() {
                    @Override public void run() {
                        for (Player p : Bukkit.getOnlinePlayers()) {
                            if (activeOrderIds.containsKey(p.getUniqueId())) {
                                if (debug) getLogger().info(
                                    "Spieler " + p.getName() + " hat aktive Order – überspringe.");
                                continue;
                            }
                            // Join-Fetch läuft noch – nicht parallel pollen
                            Long cooldown = joinFetchCooldown.get(p.getUniqueId());
                            if (cooldown != null && System.currentTimeMillis() < cooldown) {
                                if (debug) getLogger().info(
                                    "Spieler " + p.getName() + " im Join-Cooldown – überspringe.");
                                continue;
                            }
                            fetchPendingOrders(p, wpUrlPending);
                        }
                    }
                }.runTaskAsynchronously(IngameShopSpigot.this);
            }
        }.runTaskTimer(this, 20L, intervalTicks);
    }

    private void fetchPendingOrders(Player p, String endpointUrl) {
        // Vor dem API-Call prüfen – verhindert doppelte GUI bei Race Condition
        if (activeOrderIds.containsKey(p.getUniqueId())) {
            if (debug) getLogger().info(
                "fetchPendingOrders: " + p.getName() + " hat bereits aktive Order – abgebrochen.");
            return;
        }
        try {
            HttpURLConnection conn =
                openAuthConnection(endpointUrl + "?player=" + p.getName(), "GET");
            int responseCode = conn.getResponseCode();
            if (responseCode == 200) {
                String     body   = readResponse(conn);
                JsonObject json   = JsonParser.parseString(body).getAsJsonObject();
                JsonArray  orders = json.getAsJsonArray("orders");

                if (orders != null && orders.size() > 0) {
                    JsonObject order       = orders.get(0).getAsJsonObject();
                    int        id          = order.get("id").getAsInt();
                    String     orderServer = order.has("server")
                        ? order.get("server").getAsString().toLowerCase() : "";

                    if (!orderServer.equals(targetServer)) {
                        if (debug) getLogger().info(
                            "Order #" + id + " für '" + orderServer + "'. Ignoriere.");
                        return;
                    }

                    String jsonResponse = order.has("response")
                        ? order.get("response").getAsString() : "[]";
                    String itemTitle = order.get("item_title").getAsString();
                    double price     = order.get("price").getAsDouble();

                    // Atomar: nur wenn noch keine aktive Order → eintragen und GUI öffnen
                    // synchronized auf activeOrderIds verhindert Race Condition zwischen zwei Async-Threads
                    synchronized (activeOrderIds) {
                        if (activeOrderIds.containsKey(p.getUniqueId())) {
                            if (debug) getLogger().info(
                                "Order #" + id + " für " + p.getName() + " ignoriert (bereits aktiv).");
                            return;
                        }
                        OrderData data = new OrderData(id, "multi_item", itemTitle, price, 1, jsonResponse);
                        orderCache.put(id, data);
                        activeOrderIds.put(p.getUniqueId(), id);
                    }
                    Bukkit.getScheduler().runTask(IngameShopSpigot.this,
                        () -> openConfirmGUI(p, id, itemTitle, price, jsonResponse));
                }
            } else if (responseCode == 401) {
                getLogger().warning("❌ API-Key ungültig! HTTP 401 von " + endpointUrl);
            }
        } catch (Exception e) {
            if (debug) getLogger().log(Level.WARNING, "Polling Fehler für " + p.getName(), e);
        }
    }

    // ===========================================================
    // OFFLINE-QUEUE & FLY-RESTORE
    // ===========================================================

    // Spieler die gerade frisch gejoined sind – Polling überspringen bis Join-Fetch fertig
    private final Map<UUID, Long> joinFetchCooldown = new ConcurrentHashMap<>();

    @EventHandler
    public void onPlayerJoin(PlayerJoinEvent event) {
        Player p = event.getPlayer();

        // Polling für diesen Spieler sperren bis der Join-Fetch durch ist
        joinFetchCooldown.put(p.getUniqueId(), System.currentTimeMillis() + 10_000L);

        // Offline-Bestellungen abholen
        new BukkitRunnable() {
            @Override public void run() {
                fetchPendingOrders(p, wpUrlPendingOffline);
                // Sperre aufheben sobald Join-Fetch fertig
                joinFetchCooldown.remove(p.getUniqueId());
            }
        }.runTaskAsynchronously(this);

        // Fly-Zeit wiederherstellen
        flyManager.restoreOnJoin(p);

        // Abgelaufene Ränge prüfen
        new BukkitRunnable() {
            @Override public void run() { rankManager.checkExpiredRanksForPlayer(p); }
        }.runTaskAsynchronously(this);

        // Fly-Abo prüfen und ggf. Fly für heute bereitstellen
        new BukkitRunnable() {
            @Override public void run() { flyAboManager.onPlayerJoin(p); }
        }.runTaskAsynchronously(this);

        // Plot-Slot Limit anwenden (nur auf Plot-Server)
        if (targetServer.equalsIgnoreCase(plotServer)) {
            new BukkitRunnable() {
                @Override public void run() { plotSlotManager.onPlayerJoin(p); }
            }.runTaskAsynchronously(this);
        }

        // Ausstehende Fly-Codes anzeigen
        new BukkitRunnable() {
            @Override public void run() {
                List<FlyCodeManager.FlyCodeEntry> pending =
                    flyCodeManager.getUnusedCodes(p.getName());
                if (!pending.isEmpty()) {
                    Bukkit.getScheduler().runTaskLater(IngameShopSpigot.this, () ->
                        p.sendMessage(ChatColor.GOLD + "✈ Du hast "
                            + ChatColor.YELLOW + pending.size()
                            + ChatColor.GOLD   + " ungenutzten Fly-Gutschein(e)! "
                            + ChatColor.GRAY   + "Öffne sie mit: "
                            + ChatColor.WHITE  + "/flycodes"),
                    40L);
                }
            }
        }.runTaskAsynchronously(this);
    }

    @EventHandler
    public void onPlayerQuit(org.bukkit.event.player.PlayerQuitEvent event) {
        joinFetchCooldown.remove(event.getPlayer().getUniqueId());
        flyManager.saveOnQuit(event.getPlayer());
    }

    // ===========================================================
    // ANVIL – Text verfolgen & Kosten auf 0 setzen
    // ===========================================================

    // ===========================================================
    // ANVIL – Text verfolgen & Kosten auf 0 setzen
    // ===========================================================

    @EventHandler
    public void onPrepareAnvil(PrepareAnvilEvent event) {
        if (!(event.getView().getPlayer() instanceof Player)) return;
        Player p = (Player) event.getView().getPlayer();
        if (!pendingTransferAnvil.containsKey(p.getUniqueId())) return;

        AnvilInventory inv  = event.getInventory();
        String         text = inv.getRenameText();
        if (text == null) text = "";

        // Eingetippten Text immer speichern (auch wenn leer – dann alten behalten)
        if (!text.isEmpty()) {
            anvilTypedText.put(p.getUniqueId(), text);
        }

        // Result-Item mit dem aktuellen Text setzen, damit der Spieler es anklicken kann
        String display = !text.isEmpty()
            ? text
            : anvilTypedText.getOrDefault(p.getUniqueId(), "");

        if (!display.isEmpty()) {
            ItemStack result = new ItemStack(Material.PAPER);
            ItemMeta  rm     = result.getItemMeta();
            rm.setDisplayName(display);
            result.setItemMeta(rm);
            event.setResult(result);
        }

        inv.setRepairCost(0);
    }

    private static final String GUI_CONFIRM_TITLE = ChatColor.YELLOW + "Kauf bestätigen?";

    @EventHandler
    public void onInventoryClose(InventoryCloseEvent event) {
        if (!(event.getPlayer() instanceof Player)) return;
        Player p = (Player) event.getPlayer();

        // Confirm-GUI: bei ESC sofort wieder öffnen
        String title = event.getView().getTitle();
        if (GUI_CONFIRM_TITLE.equals(title) && activeOrderIds.containsKey(p.getUniqueId())) {
            int       orderId = activeOrderIds.get(p.getUniqueId());
            OrderData data    = orderCache.get(orderId);
            if (data != null) {
                p.sendMessage(ChatColor.YELLOW + "⚠ Bitte bestätige oder storniere deinen Kauf zuerst!");
                Bukkit.getScheduler().runTaskLater(this,
                    () -> openConfirmGUI(p, orderId, data.itemTitle, data.price, data.jsonPayload),
                    2L);
                return;
            }
        }

        // Anvil-Transfer: verzögert aufräumen
        if (event.getInventory().getType() == InventoryType.ANVIL
                && pendingTransferAnvil.containsKey(p.getUniqueId())) {
            Bukkit.getScheduler().runTaskLater(this, () -> {
                pendingTransferAnvil.remove(p.getUniqueId());
                anvilTypedText.remove(p.getUniqueId());
            }, 2L);
        }
    }

    // ===========================================================
    // CONFIRM GUI
    // ===========================================================

    private void openConfirmGUI(Player player, int orderId,
                                String itemTitle, double price, String jsonPayload) {
        Inventory gui  = Bukkit.createInventory(null, 27, GUI_CONFIRM_TITLE);
        Material  icon = getFirstItemMaterial(jsonPayload);

        ItemStack info     = new ItemStack(icon);
        ItemMeta  infoMeta = info.getItemMeta();
        infoMeta.setDisplayName(ChatColor.GOLD + "" + ChatColor.BOLD + itemTitle);
        infoMeta.setLore(Arrays.asList(
            ChatColor.GRAY  + "──────────────────",
            ChatColor.WHITE + "Preis: " + ChatColor.YELLOW + price + " " + currency,
            ChatColor.GRAY  + "──────────────────",
            ChatColor.GRAY  + "Klicke Grün zum Bestätigen",
            ChatColor.GRAY  + "Klicke Rot zum Abbrechen"
        ));
        info.setItemMeta(infoMeta);

        ItemStack yes = new ItemStack(Material.LIME_WOOL);
        ItemMeta  yM  = yes.getItemMeta();
        yM.setDisplayName(ChatColor.GREEN + "" + ChatColor.BOLD + "✔ JA, kaufen!");
        yM.setLore(Arrays.asList(
            ChatColor.GRAY + "" + price + " " + currency + " werden abgezogen"));
        yes.setItemMeta(yM);

        ItemStack no  = new ItemStack(Material.RED_WOOL);
        ItemMeta  nM  = no.getItemMeta();
        nM.setDisplayName(ChatColor.RED + "" + ChatColor.BOLD + "✘ NEIN, abbrechen");
        nM.setLore(Arrays.asList(ChatColor.GRAY + "Bestellung wird storniert"));
        no.setItemMeta(nM);

        ItemStack pane = new ItemStack(Material.BLACK_STAINED_GLASS_PANE);
        ItemMeta  pM   = pane.getItemMeta();
        pM.setDisplayName(" ");
        pane.setItemMeta(pM);

        for (int i = 0; i < 27; i++) gui.setItem(i, pane);
        gui.setItem(13, info);
        gui.setItem(11, yes);
        gui.setItem(15, no);
        player.openInventory(gui);
    }

    private Material getFirstItemMaterial(String jsonPayload) {
        try {
            JsonElement root  = gson.fromJson(jsonPayload, JsonElement.class);
            JsonArray   items = null;
            if (root.isJsonObject()) {
                JsonObject obj = root.getAsJsonObject();
                if (obj.has("items")) items = obj.getAsJsonArray("items");
                if ((items == null || items.size() == 0) && obj.has("commands"))
                    return Material.FEATHER;
            } else if (root.isJsonArray()) {
                items = root.getAsJsonArray();
            }
            if (items != null && items.size() > 0) {
                String    id   = items.get(0).getAsJsonObject().get("id").getAsString();
                ItemStack test = parseItem(id);
                if (test != null) return test.getType();
            }
        } catch (Exception e) {
            if (debug) getLogger().log(Level.WARNING, "getFirstItemMaterial Fehler", e);
        }
        return Material.CHEST;
    }

    // ===========================================================
    // INVENTORY CLICK
    // ===========================================================

    @EventHandler
    public void onInventoryClick(InventoryClickEvent event) {
        String title = event.getView().getTitle();

        // ── Sell GUI ──────────────────────────────────────────────────────
        if (handleSellClick(event)) return;

        // ── Fly-Codes GUI ──────────────────────────────────────────────────
        if (title.equals(GUI_FLYCODES)) {
            event.setCancelled(true);
            if (!(event.getWhoClicked() instanceof Player)) return;
            Player p    = (Player) event.getWhoClicked();
            int    slot = event.getRawSlot();

            List<FlyCodeManager.FlyCodeEntry> codes = flyCodesCache.get(p.getUniqueId());
            if (codes == null) return;
            int page  = flyCodesPage.getOrDefault(p.getUniqueId(), 0);
            int total = (int) Math.ceil(codes.size() / 45.0);

            if (slot == 45 && page > 0) {
                openFlyCodesGUI(p, codes, page - 1);
                return;
            }
            if (slot == 53 && page < total - 1) {
                openFlyCodesGUI(p, codes, page + 1);
                return;
            }
            if (slot < 0 || slot > 44) return;
            int codeIndex = page * 45 + slot;
            if (codeIndex >= codes.size()) return;

            FlyCodeManager.FlyCodeEntry entry   = codes.get(codeIndex);
            boolean                     isRight = event.getClick().isRightClick();

            if (!isRight) {
                if (flyRedeemDisabled) {
                    p.sendMessage(ChatColor.RED +
                        "✗ Fly-Codes können auf diesem Server nicht eingelöst werden.");
                    p.closeInventory();
                    return;
                }
                p.closeInventory();
                new BukkitRunnable() {
                    @Override public void run() {
                        int sec = flyCodeManager.redeemCode(entry.code);
                        Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
                            if (sec < 0)
                                p.sendMessage(ChatColor.RED + "❌ Code ungültig oder bereits eingelöst.");
                            else
                                flyManager.grantFly(p, sec);
                        });
                    }
                }.runTaskAsynchronously(this);
            } else {
                pendingTransferAnvil.put(p.getUniqueId(), entry.code);
                anvilTypedText.remove(p.getUniqueId());
                p.closeInventory();
                Bukkit.getScheduler().runTaskLater(IngameShopSpigot.this, () -> {
                    org.bukkit.inventory.InventoryView view = p.openAnvil(p.getLocation(), true);
                    if (view != null) {
                        // Slot 0 mit einem namenlosen Paper befüllen –
                        // nur so kann der Spieler im Amboss tippen.
                        // Kein DisplayName setzen, damit das Textfeld leer startet.
                        ItemStack paper = new ItemStack(Material.PAPER);
                        view.getTopInventory().setItem(0, paper);
                        p.updateInventory();
                    }
                }, 2L);
            }
            return;
        }


        // ── Anvil GUI – Spielername für Weitergabe ─────────────────────────
        if (event.getInventory().getType() == InventoryType.ANVIL
                && event.getWhoClicked() instanceof Player) {
            Player p = (Player) event.getWhoClicked();
            if (!pendingTransferAnvil.containsKey(p.getUniqueId())) return;
            if (event.getRawSlot() != 2) { event.setCancelled(true); return; }
            event.setCancelled(true);

            // Text aus anvilTypedText – wird in PrepareAnvilEvent zuverlässig befüllt
            String targetName = anvilTypedText.getOrDefault(p.getUniqueId(), "").trim();

            String code = pendingTransferAnvil.remove(p.getUniqueId());
            anvilTypedText.remove(p.getUniqueId());
            p.closeInventory();

            if (targetName.isEmpty()) {
                p.sendMessage(ChatColor.RED + "❌ Kein Spielername eingegeben.");
                return;
            }
            if (targetName.equalsIgnoreCase(p.getName())) {
                p.sendMessage(ChatColor.RED
                    + "❌ Du kannst dir den Gutschein nicht selbst übertragen.");
                return;
            }
            executeTransfer(p, code, targetName);
            return;
        }

        // ── Kauf-Bestätigen GUI ─────────────────────────────────────────────
        if (!title.contains("Kauf bestätigen?")) return;
        event.setCancelled(true);
        if (!(event.getWhoClicked() instanceof Player)) return;

        Player p    = (Player) event.getWhoClicked();
        int    slot = event.getRawSlot();

        if (slot == 11) {
            Integer orderId = activeOrderIds.remove(p.getUniqueId());
            if (orderId != null) {
                processOrder(p, orderId);
                p.closeInventory();
            } else {
                p.sendMessage(ChatColor.RED + "❌ Fehler: Kauf abgelaufen.");
                p.closeInventory();
            }
        } else if (slot == 15) {
            Integer orderId = activeOrderIds.remove(p.getUniqueId());
            if (orderId != null) {
                cancelOrder(p, orderId);
                p.closeInventory();
            }
        }
    }

    // ===========================================================
    // FLY-CODES GUI
    // ===========================================================

    private void openFlyCodesGUI(Player player,
                                  List<FlyCodeManager.FlyCodeEntry> codes, int page) {
        final int pageSize   = 45;
        final int totalPages = Math.max(1, (int) Math.ceil(codes.size() / (double) pageSize));
        page = Math.max(0, Math.min(page, totalPages - 1));

        flyCodesPage.put(player.getUniqueId(), page);
        flyCodesCache.put(player.getUniqueId(), codes);

        Inventory gui = Bukkit.createInventory(null, 54, GUI_FLYCODES);

        ItemStack pane = new ItemStack(Material.BLACK_STAINED_GLASS_PANE);
        ItemMeta  pM   = pane.getItemMeta();
        pM.setDisplayName(" ");
        pane.setItemMeta(pM);
        for (int i = 45; i < 54; i++) gui.setItem(i, pane);

        int start = page * pageSize;
        int end   = Math.min(start + pageSize, codes.size());
        for (int i = start; i < end; i++) {
            FlyCodeManager.FlyCodeEntry e    = codes.get(i);
            ItemStack                   item = new ItemStack(Material.FEATHER);
            ItemMeta                    meta = item.getItemMeta();
            meta.setDisplayName(ChatColor.YELLOW + "" + ChatColor.BOLD + e.label);

            List<String> lore = new ArrayList<>(Arrays.asList(
                ChatColor.GRAY   + "──────────────────────",
                ChatColor.GRAY   + "Code: "     + ChatColor.AQUA  + ChatColor.BOLD + e.code,
                ChatColor.GRAY   + "Dauer: "    + ChatColor.WHITE + flyManager.formatTime(e.durationSec),
                ChatColor.GRAY   + "Erhalten: " + ChatColor.WHITE + e.createdAt,
                ChatColor.GRAY   + "──────────────────────"
            ));
            if (flyRedeemDisabled) {
                lore.add(ChatColor.RED    + "✗ Einlösen auf diesem Server nicht möglich");
                lore.add(ChatColor.YELLOW + "▶ Rechtsklick: " + ChatColor.WHITE + "An Spieler weitergeben");
            } else {
                lore.add(ChatColor.GREEN  + "▶ Linksklick:  " + ChatColor.WHITE + "Selbst einlösen");
                lore.add(ChatColor.YELLOW + "▶ Rechtsklick: " + ChatColor.WHITE + "An Spieler weitergeben");
            }
            meta.setLore(lore);
            item.setItemMeta(meta);
            gui.setItem(i - start, item);
        }

        ItemStack info = new ItemStack(Material.PAPER);
        ItemMeta  im   = info.getItemMeta();
        im.setDisplayName(ChatColor.WHITE + "Seite " + (page + 1) + " / " + totalPages);
        im.setLore(Arrays.asList(
            ChatColor.GRAY      + (codes.size() + " Gutschein(e) insgesamt"),
            ChatColor.DARK_GRAY + "Codes gehören dir und sind übertragbar"
        ));
        info.setItemMeta(im);
        gui.setItem(49, info);

        if (page > 0) {
            ItemStack prev = new ItemStack(Material.ARROW);
            ItemMeta  pm2  = prev.getItemMeta();
            pm2.setDisplayName(ChatColor.YELLOW + "◀ Vorherige Seite");
            prev.setItemMeta(pm2);
            gui.setItem(45, prev);
        }
        if (page < totalPages - 1) {
            ItemStack next = new ItemStack(Material.ARROW);
            ItemMeta  nm2  = next.getItemMeta();
            nm2.setDisplayName(ChatColor.YELLOW + "Nächste Seite ▶");
            next.setItemMeta(nm2);
            gui.setItem(53, next);
        }

        if (codes.isEmpty()) {
            ItemStack empty = new ItemStack(Material.BARRIER);
            ItemMeta  em    = empty.getItemMeta();
            em.setDisplayName(ChatColor.RED + "Keine Gutscheine vorhanden");
            em.setLore(Arrays.asList(
                ChatColor.GRAY + "Kaufe Fly-Zeit im Shop,",
                ChatColor.GRAY + "um Gutscheine zu erhalten."
            ));
            empty.setItemMeta(em);
            gui.setItem(22, empty);
        }

        player.openInventory(gui);
    }

    // ===========================================================
    // ORDER PROCESSING
    // ===========================================================

    private void processOrder(Player player, int orderId) {
        OrderData data = orderCache.get(orderId);
        if (data == null) {
            player.sendMessage(ChatColor.RED + "❌ Fehler: Daten nicht gefunden.");
            return;
        }
        new BukkitRunnable() {
            @Override public void run() {
                try {
                    HttpURLConnection conn = openAuthConnection(wpUrlExecute, "POST");
                    writeJson(conn, "{\"id\":\"" + orderId + "\"}");
                    int code = conn.getResponseCode();
                    if (code == 200) {
                        Bukkit.getScheduler().runTask(IngameShopSpigot.this,
                            () -> executeShopLogic(player, data, orderId));
                    } else if (code == 401) {
                        getLogger().warning("❌ API-Key ungültig bei /execute_order");
                        Bukkit.getScheduler().runTask(IngameShopSpigot.this,
                            () -> player.sendMessage(ChatColor.RED + "❌ Konfigurationsfehler (Auth)."));
                    } else {
                        if (debug) getLogger().warning("Execute Order HTTP " + code);
                        Bukkit.getScheduler().runTask(IngameShopSpigot.this,
                            () -> player.sendMessage(ChatColor.RED + "❌ Server-Fehler beim Starten des Kaufs."));
                    }
                } catch (Exception e) {
                    getLogger().log(Level.SEVERE, "Fehler bei /execute_order", e);
                    Bukkit.getScheduler().runTask(IngameShopSpigot.this,
                        () -> player.sendMessage(ChatColor.RED + "❌ Interner Fehler beim Kauf."));
                }
            }
        }.runTaskAsynchronously(this);
    }

    private void cancelOrder(Player player, int orderId) {
        new BukkitRunnable() {
            @Override public void run() {
                try {
                    HttpURLConnection conn = openAuthConnection(wpUrlCancel, "POST");
                    writeJson(conn, "{\"id\":\"" + orderId + "\"}");
                    int code = conn.getResponseCode();
                    orderCache.remove(orderId);
                    if (code == 200) {
                        player.sendMessage(ChatColor.YELLOW + "❌ Kauf abgebrochen.");
                    } else {
                        if (debug) getLogger().warning("⚠️ Cancel HTTP " + code);
                        player.sendMessage(ChatColor.RED + "❌ Fehler beim Abbrechen.");
                        activeOrderIds.remove(player.getUniqueId());
                    }
                } catch (Exception e) {
                    getLogger().log(Level.WARNING, "Fehler bei /cancel_order", e);
                    orderCache.remove(orderId);
                    activeOrderIds.remove(player.getUniqueId());
                    player.sendMessage(ChatColor.YELLOW + "❌ Kauf abgebrochen (lokal).");
                }
            }
        }.runTaskAsynchronously(this);
    }

    private void executeShopLogic(Player player, OrderData data, int orderId) {
        try {
            if (econ.getBalance(player) < data.price) {
                player.sendMessage(ChatColor.RED + "❌ Nicht genug "
                    + currency + "! (Benötigt: " + data.price + ")");
                cancelOrder(player, orderId);
                return;
            }

            econ.withdrawPlayer(player, data.price);
            player.sendMessage(ChatColor.GREEN + "💰 " + data.price + " " + currency + " abgezogen.");

            // Einnahmen an Empfänger-Account gutschreiben
            if (!incomeReceiver.isEmpty()) {
                try {
                    @SuppressWarnings("deprecation")
                    org.bukkit.OfflinePlayer receiver =
                        Bukkit.getOfflinePlayerIfCached(incomeReceiver);
                    if (receiver == null) {
                        for (org.bukkit.OfflinePlayer op : Bukkit.getOfflinePlayers()) {
                            if (op.getName() != null
                                    && op.getName().equalsIgnoreCase(incomeReceiver)) {
                                receiver = op;
                                break;
                            }
                        }
                    }
                    if (receiver != null && receiver.hasPlayedBefore()) {
                        econ.depositPlayer(receiver, data.price);
                        if (debug) getLogger().info("💰 " + data.price + " " + currency
                            + " an " + incomeReceiver + " gutgeschrieben (Order #" + data.id + ")");
                    } else {
                        getLogger().warning("⚠ income-receiver '" + incomeReceiver
                            + "' nicht gefunden – Einnahmen nicht gutgeschrieben!");
                    }
                } catch (Exception e) {
                    getLogger().log(Level.WARNING, "Fehler beim Gutschreiben der Einnahmen", e);
                }
            }

            JsonElement root    = gson.fromJson(data.jsonPayload, JsonElement.class);
            JsonObject  rootObj = root.isJsonObject() ? root.getAsJsonObject() : null;

            // --- Normale Items ---
            JsonArray items = null;
            if (rootObj != null && rootObj.has("items")) {
                items = rootObj.getAsJsonArray("items");
            } else if (root.isJsonArray()) {
                items = root.getAsJsonArray();
            }
            int totalGiven = 0;
            if (items != null) {
                for (JsonElement e : items) {
                    JsonObject itemObj = e.getAsJsonObject();
                    String     itemId  = itemObj.get("id").getAsString();
                    int        amount  = itemObj.get("amount").getAsInt();
                    ItemStack  item    = parseItem(itemId);
                    if (item != null) { giveItems(player, item, amount); totalGiven++; }
                    else player.sendMessage(ChatColor.RED + "❌ Item '" + itemId + "' nicht gefunden.");
                }
            }

            // --- Commands (Fly / Rang / Generic) ---
            boolean anyCode = false;
            if (rootObj != null && rootObj.has("commands")) {
                JsonArray commands = rootObj.getAsJsonArray("commands");
                for (JsonElement ce : commands) {
                    JsonObject cmdObj = ce.getAsJsonObject();
                    String     type   = cmdObj.has("type")
                        ? cmdObj.get("type").getAsString() : "generic";

                    if ("fly".equals(type)) {
                        int    durationSec = cmdObj.has("duration_sec")
                            ? cmdObj.get("duration_sec").getAsInt() : 300;
                        String label = cmdObj.has("label")
                            ? cmdObj.get("label").getAsString()
                            : flyManager.formatTime(durationSec) + " Fly";
                        final String pName = player.getName();
                        new BukkitRunnable() {
                            @Override public void run() {
                                String code = flyCodeManager.generateCode(pName, durationSec, label);
                                Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
                                    player.sendMessage("");
                                    player.sendMessage(ChatColor.GOLD   + "✈ ════════════════════════════");
                                    player.sendMessage(ChatColor.YELLOW + "  Fly-Gutschein erhalten!");
                                    player.sendMessage(ChatColor.GRAY   + "  Dauer: "
                                        + ChatColor.WHITE + label);
                                    player.sendMessage(ChatColor.GRAY   + "  Code:  "
                                        + ChatColor.AQUA + ChatColor.BOLD + code);
                                    player.sendMessage(ChatColor.GRAY   + "  Einlösen: "
                                        + ChatColor.WHITE + "/flyredeem " + code);
                                    player.sendMessage(ChatColor.GOLD   + "  ════════════════════════════");
                                    player.sendMessage("");
                                    player.playSound(player.getLocation(),
                                        Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 1.2F);
                                });
                            }
                        }.runTaskAsynchronously(IngameShopSpigot.this);
                        anyCode = true;

                    } else if ("rank".equals(type)) {
                        String rankId      = cmdObj.has("rank_id")
                            ? cmdObj.get("rank_id").getAsString() : "";
                        String lpGroup     = cmdObj.has("lp_group")
                            ? cmdObj.get("lp_group").getAsString() : rankId;
                        String label       = cmdObj.has("label")
                            ? cmdObj.get("label").getAsString() : rankId;
                        String defaultGroup = cmdObj.has("default_group")
                            ? cmdObj.get("default_group").getAsString() : "default";
                        int    days        = cmdObj.has("days")
                            ? cmdObj.get("days").getAsInt() : 0;
                        if (rankId.isEmpty() || lpGroup.isEmpty()) {
                            player.sendMessage(ChatColor.RED + "❌ Rang-Konfiguration fehlt im Shop-Eintrag.");
                        } else {
                            rankManager.grantRank(player, rankId, lpGroup, label, defaultGroup, days);
                        }
                        anyCode = true;

                    } else if ("fly_abo".equals(type)) {
                        // Fly-Abo: monatliche Abbuchung
                        String label = cmdObj.has("label")
                            ? cmdObj.get("label").getAsString() : "Fly-Abo";
                        int monthlyPrice = cmdObj.has("price") && cmdObj.get("price").getAsInt() > 0
                            ? cmdObj.get("price").getAsInt()
                            : (int) data.price;
                        // Sicherer Spielername: getName() kann bei CMI leer sein → UUID-Lookup
                        String safeName = player.getName();
                        if (safeName == null || safeName.isEmpty()) {
                            org.bukkit.OfflinePlayer op = Bukkit.getOfflinePlayer(player.getUniqueId());
                            safeName = op.getName() != null ? op.getName() : player.getUniqueId().toString();
                        }
                        final String finalName = safeName;
                        getLogger().info("[FlyAbo] Abo aktiviert für " + finalName + " – " + label);
                        flyAboManager.grantAboByName(finalName, label, monthlyPrice);
                        // Ingame-Nachricht direkt an den Spieler
                        flyAboManager.notifyGranted(player, label, monthlyPrice);
                        anyCode = true;

                    } else if ("plot_slots".equals(type)) {
                        // Einmalige Plot-Slot Erweiterung (permanent)
                        int slots = cmdObj.has("slots")
                            ? cmdObj.get("slots").getAsInt() : 1;
                        String label = cmdObj.has("label")
                            ? cmdObj.get("label").getAsString() : slots + " Extra-Plot-Slot(s)";
                        plotSlotManager.grantExtraSlots(player, slots, label);
                        anyCode = true;

                    } else if ("plot_abo".equals(type)) {
                        // Monatliches Plot-Abo
                        int slots = cmdObj.has("slots")
                            ? cmdObj.get("slots").getAsInt() : 1;
                        String label = cmdObj.has("label")
                            ? cmdObj.get("label").getAsString() : "Plot-Abo +" + slots;
                        int monthlyPrice = (int) data.price;
                        plotSlotManager.grantAbo(player, slots, label, monthlyPrice);
                        anyCode = true;

                    } else if ("generic".equals(type) && cmdObj.has("cmd")) {
                        String cmd = cmdObj.get("cmd").getAsString()
                            .replace("{player}", player.getName());
                        Bukkit.dispatchCommand(Bukkit.getConsoleSender(), cmd);
                        anyCode = true;
                    }
                }
            }

            if (totalGiven > 0) {
                player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 1.0F);
                player.sendMessage(ChatColor.GREEN + "✅ Kauf erfolgreich abgeschlossen!");
            }
            if (!anyCode && totalGiven == 0) {
                player.sendMessage(ChatColor.RED + "⚠ Nichts verteilt – Admin kontaktieren.");
            }

            markOrderCompleted(orderId);
            orderCache.remove(orderId);

        } catch (Exception e) {
            getLogger().log(Level.SEVERE, "Fehler bei Shop-Logik", e);
            player.sendMessage(ChatColor.RED
                + "❌ Interner Fehler beim Verteilen. Admin kontaktieren.");
        }
    }

    private void giveItems(Player player, ItemStack template, int amount) {
        int remaining = amount;
        while (remaining > 0) {
            int       stackSize = Math.min(remaining, template.getMaxStackSize());
            ItemStack stack     = template.clone();
            stack.setAmount(stackSize);
            if (player.getInventory().firstEmpty() == -1) {
                player.getWorld().dropItemNaturally(player.getLocation(), stack);
                player.sendMessage(ChatColor.YELLOW + "⚠ Inventar voll – Items wurden gedroppt.");
            } else {
                player.getInventory().addItem(stack);
            }
            remaining -= stackSize;
        }
    }

    private void markOrderCompleted(int orderId) {
        new BukkitRunnable() {
            @Override public void run() {
                try {
                    HttpURLConnection conn = openAuthConnection(wpUrlComplete, "POST");
                    writeJson(conn, "{\"id\":\"" + orderId + "\"}");
                    int code = conn.getResponseCode();
                    if (debug) getLogger().info(
                        "Complete Order #" + orderId + " → HTTP " + code);
                } catch (Exception e) {
                    getLogger().log(Level.WARNING, "Complete Order Error #" + orderId, e);
                }
            }
        }.runTaskAsynchronously(this);
    }

    // ===========================================================
    // BEFEHLE
    // ===========================================================

    /** /orders */
    private class OrdersCommand implements CommandExecutor {
        @Override
        public boolean onCommand(CommandSender sender, Command command,
                                  String label, String[] args) {
            if (!(sender instanceof Player)) { sender.sendMessage("Nur für Spieler."); return true; }
            Player p = (Player) sender;
            p.sendMessage(ChatColor.GOLD   + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
            p.sendMessage(ChatColor.YELLOW + "📦 Deine letzten Bestellungen:");
            p.sendMessage(ChatColor.GOLD   + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
            new BukkitRunnable() {
                @Override public void run() {
                    try {
                        HttpURLConnection conn = openAuthConnection(
                            wpBase + "/orders_history?player=" + p.getName(), "GET");
                        int code = conn.getResponseCode();
                        if (code == 200) {
                            String     body   = readResponse(conn);
                            JsonObject json   = JsonParser.parseString(body).getAsJsonObject();
                            JsonArray  orders = json.getAsJsonArray("orders");
                            Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
                                if (orders == null || orders.size() == 0) {
                                    p.sendMessage(ChatColor.GRAY + "Noch keine Bestellungen vorhanden.");
                                } else {
                                    for (int i = 0; i < Math.min(orders.size(), 10); i++) {
                                        JsonObject o      = orders.get(i).getAsJsonObject();
                                        String     t      = o.get("item_title").getAsString();
                                        double     pr     = o.get("price").getAsDouble();
                                        String     status = o.get("status").getAsString();
                                        String     date   = o.has("created_at")
                                            ? o.get("created_at").getAsString().substring(0, 10) : "?";
                                        String sc;
                                        switch (status) {
                                            case "completed":  sc = ChatColor.GREEN  + "✔ Geliefert";  break;
                                            case "cancelled":  sc = ChatColor.RED    + "✘ Storniert";  break;
                                            case "processing": sc = ChatColor.AQUA   + "⟳ In Arbeit";  break;
                                            default:           sc = ChatColor.YELLOW + "⌛ Ausstehend"; break;
                                        }
                                        String display = t.length() > 35
                                            ? t.substring(0, 32) + "…" : t;
                                        p.sendMessage(
                                            ChatColor.WHITE + " #" + o.get("id").getAsInt() + " "
                                            + ChatColor.AQUA  + display
                                            + ChatColor.GRAY  + " | "
                                            + ChatColor.YELLOW + pr + " " + currency
                                            + ChatColor.GRAY  + " | " + sc
                                            + ChatColor.GRAY  + " (" + date + ")");
                                    }
                                }
                                p.sendMessage(ChatColor.GOLD + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
                            });
                        } else if (code == 404) {
                            Bukkit.getScheduler().runTask(IngameShopSpigot.this, () ->
                                p.sendMessage(ChatColor.GRAY
                                    + "Bestellhistorie nicht verfügbar (WP-Plugin zu alt?)."));
                        } else {
                            Bukkit.getScheduler().runTask(IngameShopSpigot.this, () ->
                                p.sendMessage(ChatColor.RED
                                    + "Fehler beim Laden (HTTP " + code + ")."));
                        }
                    } catch (Exception e) {
                        getLogger().log(Level.WARNING, "/orders Fehler", e);
                        Bukkit.getScheduler().runTask(IngameShopSpigot.this, () ->
                            p.sendMessage(ChatColor.RED + "Verbindungsfehler zum Shop-Server."));
                    }
                }
            }.runTaskAsynchronously(IngameShopSpigot.this);
            return true;
        }
    }

    /** /flytime */
    private class FlyTimeCommand implements CommandExecutor {
        @Override
        public boolean onCommand(CommandSender sender, Command command,
                                  String label, String[] args) {
            if (!(sender instanceof Player)) { sender.sendMessage("Nur für Spieler."); return true; }
            Player p         = (Player) sender;
            int    remaining = flyManager.getRemainingSeconds(p.getUniqueId());
            if (remaining <= 0)
                p.sendMessage(ChatColor.GRAY
                    + "✈ Kein aktiver Fly. Codes einlösen: /flyredeem <Code>");
            else
                p.sendMessage(ChatColor.AQUA + "✈ Verbleibende Fly-Zeit: "
                    + ChatColor.YELLOW + flyManager.formatTime(remaining));
            return true;
        }
    }

    /** /flyredeem <Code> */
    private class FlyRedeemCommand implements CommandExecutor {
        @Override
        public boolean onCommand(CommandSender sender, Command command,
                                  String label, String[] args) {
            if (!(sender instanceof Player)) { sender.sendMessage("Nur für Spieler."); return true; }
            if (args.length < 1) {
                sender.sendMessage(ChatColor.RED + "Verwendung: /flyredeem <Code>");
                return true;
            }
            Player p = (Player) sender;
            if (flyRedeemDisabled) {
                p.sendMessage(ChatColor.RED
                    + "✗ Fly-Codes können auf diesem Server nicht eingelöst werden.");
                return true;
            }
            String code = args[0].toUpperCase().trim();
            new BukkitRunnable() {
                @Override public void run() {
                    int durationSec = flyCodeManager.redeemCode(code);
                    Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
                        if (durationSec < 0)
                            p.sendMessage(ChatColor.RED
                                + "❌ Ungültiger oder bereits eingelöster Code.");
                        else
                            flyManager.grantFly(p, durationSec);
                    });
                }
            }.runTaskAsynchronously(IngameShopSpigot.this);
            return true;
        }
    }

    /** /flycodes */
    private class FlyCodesCommand implements CommandExecutor {
        @Override
        public boolean onCommand(CommandSender sender, Command command,
                                  String label, String[] args) {
            if (!(sender instanceof Player)) { sender.sendMessage("Nur für Spieler."); return true; }
            Player p = (Player) sender;
            new BukkitRunnable() {
                @Override public void run() {
                    List<FlyCodeManager.FlyCodeEntry> codes =
                        flyCodeManager.getUnusedCodes(p.getName());
                    Bukkit.getScheduler().runTask(IngameShopSpigot.this,
                        () -> openFlyCodesGUI(p, codes, 0));
                }
            }.runTaskAsynchronously(IngameShopSpigot.this);
            return true;
        }
    }

    /** /flygive <Spieler> <Sekunden> [Label] */
    private class FlyGiveCommand implements CommandExecutor {
        @Override
        public boolean onCommand(CommandSender sender, Command command,
                                  String label, String[] args) {
            if (!sender.hasPermission("ingameshop.flygive")) {
                sender.sendMessage(ChatColor.RED + "✗ Keine Berechtigung.");
                return true;
            }
            if (args.length < 2) {
                sender.sendMessage(ChatColor.RED
                    + "Verwendung: /flygive <Spieler> <Sekunden> [Label]");
                return true;
            }
            String targetName = args[0];
            int    durationSec;
            try {
                durationSec = Integer.parseInt(args[1]);
                if (durationSec <= 0) throw new NumberFormatException();
            } catch (NumberFormatException e) {
                sender.sendMessage(ChatColor.RED
                    + "✗ Ungültige Sekundenangabe. Bitte eine positive Zahl eingeben.");
                return true;
            }
            String customLabel = args.length >= 3
                ? String.join(" ", Arrays.copyOfRange(args, 2, args.length))
                : flyManager.formatTime(durationSec) + " Fly";

            final int    finalSec   = durationSec;
            final String finalLabel = customLabel;
            Player       online     = Bukkit.getPlayer(targetName);

            if (online != null) {
                final String resolvedName = online.getName();
                new BukkitRunnable() {
                    @Override public void run() {
                        String code = flyCodeManager.generateCode(resolvedName, finalSec, finalLabel);
                        Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
                            sender.sendMessage(ChatColor.GREEN + "✅ Fly-Gutschein an "
                                + ChatColor.YELLOW + resolvedName + ChatColor.GREEN + " vergeben!");
                            sender.sendMessage(ChatColor.GRAY + "   Code:  "
                                + ChatColor.AQUA + ChatColor.BOLD + code);
                            sender.sendMessage(ChatColor.GRAY + "   Dauer: "
                                + ChatColor.WHITE + flyManager.formatTime(finalSec));
                            sender.sendMessage(ChatColor.GRAY + "   Label: "
                                + ChatColor.WHITE + finalLabel);
                            online.sendMessage("");
                            online.sendMessage(ChatColor.GOLD   + "✈ ════════════════════════════");
                            online.sendMessage(ChatColor.YELLOW + "  Du hast einen Fly-Gutschein erhalten!");
                            online.sendMessage(ChatColor.GRAY   + "  Von: "
                                + ChatColor.WHITE + sender.getName());
                            online.sendMessage(ChatColor.GRAY   + "  Dauer: "
                                + ChatColor.WHITE + flyManager.formatTime(finalSec));
                            online.sendMessage(ChatColor.GRAY   + "  Code:  "
                                + ChatColor.AQUA + ChatColor.BOLD + code);
                            online.sendMessage(ChatColor.GRAY   + "  Einlösen: "
                                + ChatColor.WHITE + "/flyredeem " + code);
                            online.sendMessage(ChatColor.GOLD   + "  ════════════════════════════");
                            online.sendMessage("");
                            online.playSound(online.getLocation(),
                                Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 1.2F);
                        });
                    }
                }.runTaskAsynchronously(IngameShopSpigot.this);
            } else {
                new BukkitRunnable() {
                    @Override public void run() {
                        boolean knownTemp = false;
                        for (org.bukkit.OfflinePlayer op : Bukkit.getOfflinePlayers()) {
                            if (op.getName() != null
                                    && op.getName().equalsIgnoreCase(targetName)) {
                                knownTemp = true;
                                break;
                            }
                        }
                        final boolean known = knownTemp;
                        String resolved = targetName;
                        for (org.bukkit.OfflinePlayer op : Bukkit.getOfflinePlayers()) {
                            if (op.getName() != null
                                    && op.getName().equalsIgnoreCase(targetName)) {
                                resolved = op.getName();
                                break;
                            }
                        }
                        final String finalResolved = resolved;
                        String code = flyCodeManager.generateCode(finalResolved, finalSec, finalLabel);
                        Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
                            sender.sendMessage(ChatColor.GREEN + "✅ Fly-Gutschein für "
                                + ChatColor.YELLOW + finalResolved
                                + ChatColor.GREEN  + " gespeichert"
                                + (known ? "" : ChatColor.GRAY + " (Spieler noch nie online)")
                                + ChatColor.GREEN + "!");
                            sender.sendMessage(ChatColor.GRAY + "   Code:  "
                                + ChatColor.AQUA + ChatColor.BOLD + code);
                            sender.sendMessage(ChatColor.GRAY + "   Dauer: "
                                + ChatColor.WHITE + flyManager.formatTime(finalSec));
                            sender.sendMessage(ChatColor.GRAY + "   Label: "
                                + ChatColor.WHITE + finalLabel);
                            sender.sendMessage(ChatColor.GRAY
                                + "   → Spieler wird beim nächsten Login benachrichtigt.");
                        });
                    }
                }.runTaskAsynchronously(IngameShopSpigot.this);
            }
            return true;
        }
    }

    /** /flypause */
    private class FlyPauseCommand implements CommandExecutor {
        @Override
        public boolean onCommand(CommandSender sender, Command command,
                                  String label, String[] args) {
            if (!(sender instanceof Player)) { sender.sendMessage("Nur für Spieler."); return true; }
            Player p = (Player) sender;
            if (flyManager.getRemainingSeconds(p.getUniqueId()) <= 0) {
                p.sendMessage(ChatColor.RED + "✗ Du hast keine aktive Fly-Zeit.");
                return true;
            }
            boolean nowPaused = flyManager.togglePause(p);
            if (nowPaused)
                p.sendMessage(ChatColor.YELLOW + "⏸ Fly-Zeit pausiert. "
                    + ChatColor.GRAY + "Verbleibend: "
                    + ChatColor.WHITE + flyManager.formatTime(
                        flyManager.getRemainingSeconds(p.getUniqueId())));
            else
                p.sendMessage(ChatColor.GREEN + "▶ Fly-Zeit fortgesetzt. "
                    + ChatColor.GRAY + "Verbleibend: "
                    + ChatColor.WHITE + flyManager.formatTime(
                        flyManager.getRemainingSeconds(p.getUniqueId())));
            return true;
        }
    }

    /** /rankinfo */
    private class RankInfoCommand implements CommandExecutor {
        @Override
        public boolean onCommand(CommandSender sender, Command command,
                                  String label, String[] args) {
            if (!(sender instanceof Player)) { sender.sendMessage("Nur für Spieler."); return true; }
            Player p = (Player) sender;
            new BukkitRunnable() {
                @Override public void run() {
                    List<RankManager.RankEntry> ranks =
                        rankManager.getActiveRanks(p.getName());
                    Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
                        p.sendMessage(ChatColor.GOLD   + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
                        p.sendMessage(ChatColor.YELLOW + "👑 Deine aktiven Ränge:");
                        p.sendMessage(ChatColor.GOLD   + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
                        if (ranks.isEmpty()) {
                            p.sendMessage(ChatColor.GRAY + "Keine zeitbasierten Ränge aktiv.");
                        } else {
                            for (RankManager.RankEntry r : ranks) {
                                if (r.permanent)
                                    p.sendMessage(ChatColor.AQUA  + " ▶ "
                                        + ChatColor.WHITE + r.rankId
                                        + ChatColor.GRAY  + " | "
                                        + ChatColor.GREEN + "Dauerhaft");
                                else
                                    p.sendMessage(ChatColor.AQUA  + " ▶ "
                                        + ChatColor.WHITE  + r.rankId
                                        + ChatColor.GRAY   + " | Läuft ab: "
                                        + ChatColor.YELLOW + r.expiresAt);
                            }
                        }
                        p.sendMessage(ChatColor.GOLD + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
                    });
                }
            }.runTaskAsynchronously(IngameShopSpigot.this);
            return true;
        }
    }

    // ===========================================================
    // FLY-ABO COMMANDS
    // ===========================================================

    /** /flyabo – Abo-Status anzeigen */
    // ===========================================================
    // /abo – ÜBERSICHT ALLER LAUFENDEN ABOS
    // ===========================================================

    private class AboOverviewCommand implements CommandExecutor {
        @Override
        public boolean onCommand(CommandSender sender, Command command,
                                  String label, String[] args) {
            if (!(sender instanceof Player)) { sender.sendMessage("Nur für Spieler."); return true; }
            Player p = (Player) sender;

            new BukkitRunnable() {
                @Override public void run() {
                    // Alle Daten async laden
                    FlyAboManager.AboEntry       flyAbo  = flyAboManager.getActiveAbo(p.getName());
                    PlotSlotManager.PlotAboEntry plotAbo = plotSlotManager.getActiveAbo(p.getName());
                    int flyUsedSec  = flyAboManager.getUsedTodaySec(p.getName());
                    int plotBase    = plotSlotManager.getRankBaseByName(p.getName());
                    int plotExtra   = plotSlotManager.getExtraSlots(p.getName());
                    int plotAboSlots= plotSlotManager.getAboSlots(p.getName());
                    int plotTotal   = plotBase + plotExtra + plotAboSlots;

                    Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
                        String line = ChatColor.GOLD + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━";

                        p.sendMessage("");
                        p.sendMessage(line);
                        p.sendMessage(ChatColor.YELLOW + "" + ChatColor.BOLD + "  📋 Meine Abonnements");
                        p.sendMessage(line);

                        boolean hasAny = false;

                        // ── Fly-Abo ──────────────────────────────────────────
                        if (flyAbo != null) {
                            hasAny = true;
                            boolean cancelled = flyAbo.cancelled;
                            boolean payFail   = "payment_failed".equals(flyAbo.cancellationReason);

                            p.sendMessage("");
                            p.sendMessage(ChatColor.AQUA + "" + ChatColor.BOLD + "  ✈ Fly-Abo");
                            p.sendMessage(ChatColor.GRAY + "  ┌ Paket:       " + ChatColor.WHITE + flyAbo.label);
                            p.sendMessage(ChatColor.GRAY + "  ├ Preis:       " + ChatColor.WHITE + flyAbo.monthlyPrice + " " + currency + " / Monat");

                            if (cancelled) {
                                p.sendMessage(ChatColor.GRAY + "  ├ Status:      " + ChatColor.RED
                                    + "⚠ Gekündigt" + (payFail ? " (Zahlung fehlgeschlagen)" : " (durch dich)"));
                                p.sendMessage(ChatColor.GRAY + "  ├ Endet am:    " + ChatColor.YELLOW + flyAbo.periodEnd);
                            } else {
                                p.sendMessage(ChatColor.GRAY + "  ├ Status:      " + ChatColor.GREEN + "✔ Aktiv");
                                p.sendMessage(ChatColor.GRAY + "  ├ Aktiv bis:   " + ChatColor.WHITE + flyAbo.periodEnd);
                                p.sendMessage(ChatColor.GRAY + "  ├ Nächste Abbuchung: " + ChatColor.WHITE + flyAbo.nextBillingDate);
                            }

                            int remSec = Math.max(0, flyAboMaxDailySec - flyUsedSec);
                            p.sendMessage(ChatColor.GRAY + "  ├ Heute:       "
                                + ChatColor.WHITE + flyManager.formatTime(flyUsedSec)
                                + ChatColor.GRAY  + " / " + ChatColor.WHITE + flyManager.formatTime(flyAboMaxDailySec)
                                + ChatColor.GRAY  + "  (noch: "
                                + (remSec > 0 ? ChatColor.GREEN : ChatColor.RED) + flyManager.formatTime(remSec) + ChatColor.GRAY + ")");

                            if (!cancelled)
                                p.sendMessage(ChatColor.GRAY + "  └ Kündigen:    " + ChatColor.WHITE + "/flyabocancel confirm");
                            else
                                p.sendMessage(ChatColor.GRAY + "  └ Erneuern:    " + ChatColor.AQUA + "viper-network.de");
                        }

                        // ── Plot-Abo ──────────────────────────────────────────
                        if (plotAbo != null) {
                            hasAny = true;
                            boolean cancelled = plotAbo.cancelled;
                            boolean payFail   = "payment_failed".equals(plotAbo.cancellationReason);

                            p.sendMessage("");
                            p.sendMessage(ChatColor.AQUA + "" + ChatColor.BOLD + "  📦 Plot-Abo");
                            p.sendMessage(ChatColor.GRAY + "  ┌ Paket:       " + ChatColor.WHITE + plotAbo.label);
                            p.sendMessage(ChatColor.GRAY + "  ├ Slots:       " + ChatColor.WHITE + "+" + plotAbo.aboSlots + " Plot-Slots");
                            p.sendMessage(ChatColor.GRAY + "  ├ Preis:       " + ChatColor.WHITE + plotAbo.monthlyPrice + " " + currency + " / Monat");

                            if (cancelled) {
                                p.sendMessage(ChatColor.GRAY + "  ├ Status:      " + ChatColor.RED
                                    + "⚠ Gekündigt" + (payFail ? " (Zahlung fehlgeschlagen)" : " (durch dich)"));
                                p.sendMessage(ChatColor.GRAY + "  └ Endet am:    " + ChatColor.YELLOW + plotAbo.periodEnd);
                            } else {
                                p.sendMessage(ChatColor.GRAY + "  ├ Status:      " + ChatColor.GREEN + "✔ Aktiv");
                                p.sendMessage(ChatColor.GRAY + "  ├ Aktiv bis:   " + ChatColor.WHITE + plotAbo.periodEnd);
                                p.sendMessage(ChatColor.GRAY + "  ├ Nächste Abbuchung: " + ChatColor.WHITE + plotAbo.nextBillingDate);
                                p.sendMessage(ChatColor.GRAY + "  └ Kündigen:    " + ChatColor.WHITE + "/plotabocancel confirm");
                            }
                        }

                        // ── Plot-Slots Übersicht (immer anzeigen) ─────────────
                        p.sendMessage("");
                        p.sendMessage(ChatColor.AQUA + "" + ChatColor.BOLD + "  📦 Plot-Slots");
                        p.sendMessage(ChatColor.GRAY + "  ┌ Rang-Basis:  " + ChatColor.WHITE + plotBase);
                        p.sendMessage(ChatColor.GRAY + "  ├ Einmalig:    " + ChatColor.WHITE + (plotExtra > 0 ? "+" + plotExtra : "0"));
                        p.sendMessage(ChatColor.GRAY + "  ├ Abo:         " + ChatColor.WHITE + (plotAboSlots > 0 ? "+" + plotAboSlots : "0"));
                        p.sendMessage(ChatColor.GRAY + "  └ Gesamt:      " + ChatColor.GREEN + "" + ChatColor.BOLD + plotTotal + " Slots");

                        // ── Kein Abo ──────────────────────────────────────────
                        if (!hasAny) {
                            p.sendMessage("");
                            p.sendMessage(ChatColor.GRAY + "  Du hast derzeit keine aktiven Abonnements.");
                            p.sendMessage(ChatColor.GRAY + "  Im Shop erhältlich: " + ChatColor.AQUA + "viper-network.de");
                        }

                        p.sendMessage("");
                        p.sendMessage(line);
                        p.sendMessage("");
                    });
                }
            }.runTaskAsynchronously(IngameShopSpigot.this);
            return true;
        }
    }

    private class FlyAboCommand implements CommandExecutor {
        @Override
        public boolean onCommand(CommandSender sender, Command command,
                                  String label, String[] args) {
            if (!(sender instanceof Player)) { sender.sendMessage("Nur für Spieler."); return true; }
            Player p = (Player) sender;
            new BukkitRunnable() {
                @Override public void run() {
                    FlyAboManager.AboEntry abo = flyAboManager.getActiveAbo(p.getName());
                    Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
                        p.sendMessage(ChatColor.GOLD   + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
                        p.sendMessage(ChatColor.YELLOW + "✈ Dein Fly-Abo:");
                        p.sendMessage(ChatColor.GOLD   + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
                        if (abo == null) {
                            p.sendMessage(ChatColor.GRAY + "  Kein aktives Fly-Abo.");
                            p.sendMessage(ChatColor.GRAY + "  Im Shop erhältlich: " + ChatColor.AQUA + "viper-network.de");
                        } else {
                            p.sendMessage(ChatColor.AQUA  + "  Paket:  " + ChatColor.WHITE + abo.label);
                            p.sendMessage(ChatColor.AQUA  + "  Preis:  " + ChatColor.WHITE + abo.monthlyPrice + " " + currency + " / Monat");
                            if (abo.cancelled) {
                                String reason = "payment_failed".equals(abo.cancellationReason)
                                    ? " (Zahlung fehlgeschlagen)" : " (durch dich)";
                                p.sendMessage(ChatColor.RED  + "  Status: ⚠ Gekündigt" + reason);
                                p.sendMessage(ChatColor.AQUA + "  Aktiv bis: " + ChatColor.YELLOW + abo.periodEnd);
                                p.sendMessage(ChatColor.GRAY + "  Danach kein Fly-Abo mehr aktiv.");
                            } else {
                                p.sendMessage(ChatColor.AQUA + "  Status: " + ChatColor.GREEN + "✔ Aktiv");
                                p.sendMessage(ChatColor.AQUA + "  Aktiv bis: "          + ChatColor.YELLOW + abo.periodEnd);
                                p.sendMessage(ChatColor.AQUA + "  Nächste Abbuchung: "  + ChatColor.WHITE  + abo.nextBillingDate);
                                p.sendMessage(ChatColor.GRAY + "  Kündigen zum Monatsende: " + ChatColor.WHITE + "/flyabocancel");
                            }
                            int usedSec = flyAboManager.getUsedTodaySec(p.getName());
                            int maxSec  = flyAboMaxDailySec;
                            int remSec  = Math.max(0, maxSec - usedSec);
                            p.sendMessage(ChatColor.AQUA + "  Heute geflogen: "
                                + ChatColor.WHITE + flyManager.formatTime(usedSec)
                                + ChatColor.GRAY  + " / "
                                + ChatColor.WHITE + flyManager.formatTime(maxSec));
                            p.sendMessage(ChatColor.AQUA + "  Heute noch verfügbar: "
                                + (remSec > 0 ? ChatColor.GREEN : ChatColor.RED)
                                + flyManager.formatTime(remSec));
                        }
                        p.sendMessage(ChatColor.GOLD + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
                    });
                }
            }.runTaskAsynchronously(IngameShopSpigot.this);
            return true;
        }
    }

    /** /flyabocancel – Abo zum Ende des laufenden Monats kündigen */
    private class FlyAboCancelCommand implements CommandExecutor {
        @Override
        public boolean onCommand(CommandSender sender, Command command,
                                  String label, String[] args) {
            if (!(sender instanceof Player)) { sender.sendMessage("Nur für Spieler."); return true; }
            Player p = (Player) sender;

            // Letzten Tag des laufenden Monats berechnen
            java.time.LocalDate today    = java.time.LocalDate.now();
            java.time.LocalDate monthEnd = today.withDayOfMonth(today.lengthOfMonth());
            String endDateStr = monthEnd.format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy"));

            // Bestätigung erforderlich
            if (args.length == 0 || !args[0].equalsIgnoreCase("confirm")) {
                p.sendMessage(ChatColor.YELLOW + "⚠ Möchtest du dein Fly-Abo wirklich kündigen?");
                p.sendMessage(ChatColor.GRAY   + "  Das Abo läuft noch bis zum " + ChatColor.WHITE + endDateStr + ChatColor.GRAY + " weiter.");
                p.sendMessage(ChatColor.GRAY   + "  Ab dem 1. des Folgemonats wird es nicht mehr verlängert.");
                p.sendMessage(ChatColor.WHITE  + "  Bestätigen: " + ChatColor.RED + "/flyabocancel confirm");
                return true;
            }
            new BukkitRunnable() {
                @Override public void run() {
                    boolean ok = flyAboManager.cancelAbo(p.getName());
                    Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
                        if (ok) {
                            p.sendMessage(ChatColor.YELLOW + "✈ Dein Fly-Abo wurde zum " + ChatColor.WHITE + endDateStr + ChatColor.YELLOW + " gekündigt.");
                            p.sendMessage(ChatColor.GRAY   + "  Bis dahin kannst du es wie gewohnt nutzen.");
                            p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1.0F, 0.8F);
                        } else {
                            p.sendMessage(ChatColor.RED + "✗ Kein aktives Fly-Abo gefunden oder bereits gekündigt.");
                        }
                    });
                }
            }.runTaskAsynchronously(IngameShopSpigot.this);
            return true;
        }
    }

    /** /flyabogive <Spieler> <Monatspreis> [Label] – Admin gibt Abo manuell */
    private class FlyAboGiveCommand implements CommandExecutor {
        @Override
        public boolean onCommand(CommandSender sender, Command command,
                                  String label, String[] args) {
            if (!sender.hasPermission("ingameshop.flyabogive")) {
                sender.sendMessage(ChatColor.RED + "✗ Keine Berechtigung.");
                return true;
            }
            if (args.length < 2) {
                sender.sendMessage(ChatColor.YELLOW + "Verwendung: /flyabogive <Spieler> <Monatspreis> [Label]");
                sender.sendMessage(ChatColor.GRAY   + "  Preis = 0 → kostenloses Abo (kein Vault-Abzug)");
                return true;
            }
            String targetName = args[0];
            int monthlyPrice;
            try { monthlyPrice = Integer.parseInt(args[1]); }
            catch (NumberFormatException e) {
                sender.sendMessage(ChatColor.RED + "✗ Monatspreis muss eine Zahl sein.");
                return true;
            }
            String aboLabel = args.length >= 3
                ? String.join(" ", Arrays.copyOfRange(args, 2, args.length))
                : "Fly-Abo";
            Player target = Bukkit.getPlayer(targetName);
            new BukkitRunnable() {
                @Override public void run() {
                    flyAboManager.grantAboByName(targetName, aboLabel, monthlyPrice);
                    Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
                        sender.sendMessage(ChatColor.GREEN + "✔ Fly-Abo an "
                            + ChatColor.YELLOW + targetName + ChatColor.GREEN + " vergeben."
                            + ChatColor.GRAY + " (Monatspreis: " + monthlyPrice + " " + currency + ")");
                        if (target != null && target.isOnline()) {
                            target.sendMessage("");
                            target.sendMessage(ChatColor.GOLD   + "✈ ════════════════════════════");
                            target.sendMessage(ChatColor.YELLOW + "  Fly-Abo aktiviert!");
                            target.sendMessage(ChatColor.GRAY   + "  Paket: "       + ChatColor.WHITE + aboLabel);
                            target.sendMessage(ChatColor.GRAY   + "  Monatspreis: " + ChatColor.WHITE + monthlyPrice + " " + currency);
                            target.sendMessage(ChatColor.GRAY   + "  Tägl. Limit: " + ChatColor.WHITE
                                + flyManager.formatTime(flyAboMaxDailySec));
                            target.sendMessage(ChatColor.GRAY   + "  Nächste Abbuchung: " + ChatColor.YELLOW + "1. des Folgemonats");
                            target.sendMessage(ChatColor.GRAY   + "  Status: /flyabo");
                            target.sendMessage(ChatColor.GOLD   + "  ════════════════════════════");
                            target.sendMessage("");
                            target.playSound(target.getLocation(),
                                Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 1.2F);
                        }
                    });
                }
            }.runTaskAsynchronously(IngameShopSpigot.this);
            return true;
        }
    }

    // ===========================================================
    // PLOT-SLOT COMMANDS
    // ===========================================================

    /** /plotslots – Gesamtstatus: Basis + extra + Abo */
    private class PlotSlotsCommand implements CommandExecutor {
        @Override
        public boolean onCommand(CommandSender sender, Command command,
                                  String label, String[] args) {
            if (!(sender instanceof Player)) { sender.sendMessage("Nur für Spieler."); return true; }
            Player p = (Player) sender;
            new BukkitRunnable() {
                @Override public void run() {
                    int base  = plotSlotManager.getRankBase(p);
                    int extra = plotSlotManager.getExtraSlots(p.getName());
                    int abo   = plotSlotManager.getAboSlots(p.getName());
                    int total = base + extra + abo;
                    Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
                        p.sendMessage(ChatColor.GOLD   + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
                        p.sendMessage(ChatColor.YELLOW + "📦 Deine Plot-Slots:");
                        p.sendMessage(ChatColor.GOLD   + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
                        p.sendMessage(ChatColor.AQUA   + "  Rang-Basis:    " + ChatColor.WHITE + base);
                        p.sendMessage(ChatColor.AQUA   + "  Einmalig:      " + ChatColor.WHITE + extra);
                        p.sendMessage(ChatColor.AQUA   + "  Abo:           " + ChatColor.WHITE + abo);
                        p.sendMessage(ChatColor.AQUA   + "  Gesamt:        " + ChatColor.GREEN + total);
                        p.sendMessage(ChatColor.GRAY   + "  Mehr Slots:    " + ChatColor.AQUA + "viper-network.de");
                        p.sendMessage(ChatColor.GOLD   + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
                    });
                }
            }.runTaskAsynchronously(IngameShopSpigot.this);
            return true;
        }
    }

    /** /plotabo – Abo-Status anzeigen */
    private class PlotAboCommand implements CommandExecutor {
        @Override
        public boolean onCommand(CommandSender sender, Command command,
                                  String label, String[] args) {
            if (!(sender instanceof Player)) { sender.sendMessage("Nur für Spieler."); return true; }
            Player p = (Player) sender;
            new BukkitRunnable() {
                @Override public void run() {
                    PlotSlotManager.PlotAboEntry abo = plotSlotManager.getActiveAbo(p.getName());
                    Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
                        p.sendMessage(ChatColor.GOLD   + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
                        p.sendMessage(ChatColor.YELLOW + "📦 Dein Plot-Abo:");
                        p.sendMessage(ChatColor.GOLD   + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
                        if (abo == null) {
                            p.sendMessage(ChatColor.GRAY + "  Kein aktives Plot-Abo.");
                            p.sendMessage(ChatColor.GRAY + "  Im Shop erhältlich: " + ChatColor.AQUA + "viper-network.de");
                        } else {
                            p.sendMessage(ChatColor.AQUA + "  Paket:  " + ChatColor.WHITE + abo.label);
                            p.sendMessage(ChatColor.AQUA + "  Slots:  " + ChatColor.WHITE + "+" + abo.aboSlots);
                            p.sendMessage(ChatColor.AQUA + "  Preis:  " + ChatColor.WHITE + abo.monthlyPrice + " " + currency + " / Monat");
                            if (abo.cancelled) {
                                String reason = "payment_failed".equals(abo.cancellationReason)
                                    ? " (Zahlung fehlgeschlagen)" : " (durch dich)";
                                p.sendMessage(ChatColor.RED  + "  Status: ⚠ Gekündigt" + reason);
                                p.sendMessage(ChatColor.AQUA + "  Aktiv bis: " + ChatColor.YELLOW + abo.periodEnd);
                            } else {
                                p.sendMessage(ChatColor.AQUA + "  Status: " + ChatColor.GREEN + "✔ Aktiv");
                                p.sendMessage(ChatColor.AQUA + "  Aktiv bis:          " + ChatColor.YELLOW + abo.periodEnd);
                                p.sendMessage(ChatColor.AQUA + "  Nächste Abbuchung:  " + ChatColor.WHITE  + abo.nextBillingDate);
                                p.sendMessage(ChatColor.GRAY + "  Kündigen: " + ChatColor.WHITE + "/plotabocancel");
                            }
                        }
                        p.sendMessage(ChatColor.GOLD + "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
                    });
                }
            }.runTaskAsynchronously(IngameShopSpigot.this);
            return true;
        }
    }

    /** /plotabocancel [confirm] – Abo zum Monatsende kündigen */
    private class PlotAboCancelCommand implements CommandExecutor {
        @Override
        public boolean onCommand(CommandSender sender, Command command,
                                  String label, String[] args) {
            if (!(sender instanceof Player)) { sender.sendMessage("Nur für Spieler."); return true; }
            Player p = (Player) sender;
            if (args.length == 0 || !args[0].equalsIgnoreCase("confirm")) {
                p.sendMessage(ChatColor.YELLOW + "⚠ Möchtest du dein Plot-Abo wirklich kündigen?");
                p.sendMessage(ChatColor.GRAY   + "  Die Abo-Slots bleiben bis Monatsende erhalten.");
                p.sendMessage(ChatColor.GRAY   + "  Plots mit überzähligen Slots werden eingefroren.");
                p.sendMessage(ChatColor.WHITE  + "  Bestätigen: " + ChatColor.RED + "/plotabocancel confirm");
                return true;
            }
            new BukkitRunnable() {
                @Override public void run() {
                    boolean ok = plotSlotManager.cancelAbo(p.getName());
                    Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
                        if (ok) {
                            p.sendMessage(ChatColor.YELLOW + "📦 Plot-Abo zur Kündigung vorgemerkt.");
                            p.sendMessage(ChatColor.GRAY   + "  Deine Abo-Slots bleiben bis Ende des Monats aktiv.");
                            p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1.0F, 0.8F);
                        } else {
                            p.sendMessage(ChatColor.RED + "✗ Kein aktives Plot-Abo gefunden.");
                        }
                    });
                }
            }.runTaskAsynchronously(IngameShopSpigot.this);
            return true;
        }
    }

    /** /plotabogive <Spieler> <Slots> <Monatspreis> [Label] – Admin */
    private class PlotAboGiveCommand implements CommandExecutor {
        @Override
        public boolean onCommand(CommandSender sender, Command command,
                                  String label, String[] args) {
            if (!sender.hasPermission("ingameshop.plotabogive")) {
                sender.sendMessage(ChatColor.RED + "✗ Keine Berechtigung.");
                return true;
            }
            if (args.length < 3) {
                sender.sendMessage(ChatColor.YELLOW + "Verwendung: /plotabogive <Spieler> <Slots> <Monatspreis> [Label]");
                return true;
            }
            String targetName = args[0];
            int slots, price;
            try { slots = Integer.parseInt(args[1]); price = Integer.parseInt(args[2]); }
            catch (NumberFormatException e) {
                sender.sendMessage(ChatColor.RED + "✗ Slots und Preis müssen Zahlen sein.");
                return true;
            }
            String aboLabel = args.length >= 4
                ? String.join(" ", Arrays.copyOfRange(args, 3, args.length))
                : "Plot-Abo +" + slots;
            Player target = Bukkit.getPlayer(targetName);
            new BukkitRunnable() {
                @Override public void run() {
                    plotSlotManager.grantAboByName(targetName, slots, aboLabel, price);
                    Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
                        sender.sendMessage(ChatColor.GREEN + "✔ Plot-Abo (+" + slots + " Slots) an "
                            + ChatColor.YELLOW + targetName + ChatColor.GREEN + " vergeben.");
                        if (target != null && target.isOnline()) {
                            target.sendMessage("");
                            target.sendMessage(ChatColor.GOLD   + "📦 ════════════════════════════");
                            target.sendMessage(ChatColor.YELLOW + "  Plot-Abo aktiviert!");
                            target.sendMessage(ChatColor.GRAY   + "  Paket: "       + ChatColor.WHITE + aboLabel);
                            target.sendMessage(ChatColor.GRAY   + "  Slots: "       + ChatColor.WHITE + "+" + slots);
                            target.sendMessage(ChatColor.GRAY   + "  Monatspreis: " + ChatColor.WHITE + price + " " + currency);
                            target.sendMessage(ChatColor.GRAY   + "  Status: /plotabo");
                            target.sendMessage(ChatColor.GOLD   + "  ════════════════════════════");
                            target.playSound(target.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 1.2F);
                        }
                    });
                }
            }.runTaskAsynchronously(IngameShopSpigot.this);
            return true;
        }
    }

    /** /plotslotsgive <Spieler> <Slots> [Label] – Admin gibt permanente Slots */
    private class PlotSlotsGiveCommand implements CommandExecutor {
        @Override
        public boolean onCommand(CommandSender sender, Command command,
                                  String label, String[] args) {
            if (!sender.hasPermission("ingameshop.plotslotsgive")) {
                sender.sendMessage(ChatColor.RED + "✗ Keine Berechtigung.");
                return true;
            }
            if (args.length < 2) {
                sender.sendMessage(ChatColor.YELLOW + "Verwendung: /plotslotsgive <Spieler> <Slots> [Label]");
                return true;
            }
            String targetName = args[0];
            int slots;
            try { slots = Integer.parseInt(args[1]); }
            catch (NumberFormatException e) {
                sender.sendMessage(ChatColor.RED + "✗ Slots muss eine Zahl sein.");
                return true;
            }
            String slotLabel = args.length >= 3
                ? String.join(" ", Arrays.copyOfRange(args, 2, args.length))
                : "+" + slots + " Plot-Slots";
            Player target = Bukkit.getPlayer(targetName);
            new BukkitRunnable() {
                @Override public void run() {
                    plotSlotManager.grantExtraSlotsByName(targetName, slots, slotLabel);
                    Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
                        sender.sendMessage(ChatColor.GREEN + "✔ " + slots + " permanente Plot-Slots an "
                            + ChatColor.YELLOW + targetName + ChatColor.GREEN + " vergeben.");
                        if (target != null && target.isOnline()) {
                            target.sendMessage("");
                            target.sendMessage(ChatColor.GOLD   + "📦 ════════════════════════════");
                            target.sendMessage(ChatColor.YELLOW + "  Plot-Slots erweitert!");
                            target.sendMessage(ChatColor.GRAY   + "  Paket: " + ChatColor.WHITE + slotLabel);
                            target.sendMessage(ChatColor.GRAY   + "  Dauerhaft: +" + slots + " Slots");
                            target.sendMessage(ChatColor.GRAY   + "  Status: /plotslots");
                            target.sendMessage(ChatColor.GOLD   + "  ════════════════════════════");
                            target.playSound(target.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 1.0F);
                        }
                    });
                }
            }.runTaskAsynchronously(IngameShopSpigot.this);
            return true;
        }
    }

    // ===========================================================
    // HTTP HILFSMETHODEN
    // ===========================================================

    private HttpURLConnection openAuthConnection(String urlString, String method)
            throws Exception {
        URL               url  = new URL(urlString);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod(method);
        conn.setRequestProperty("Content-Type", "application/json");
        conn.setRequestProperty("X-WIS-Key", apiKey);
        conn.setConnectTimeout(5000);
        conn.setReadTimeout(5000);
        if ("POST".equals(method)) conn.setDoOutput(true);
        return conn;
    }

    private void writeJson(HttpURLConnection conn, String json) throws Exception {
        try (OutputStream os = conn.getOutputStream()) {
            os.write(json.getBytes(StandardCharsets.UTF_8));
        }
    }

    private String readResponse(HttpURLConnection conn) throws Exception {
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) sb.append(line);
            return sb.toString();
        }
    }

    // ===========================================================
    // ITEM PARSING
    // ===========================================================

    private ItemStack parseItem(String itemId) {
        try {
            String clean        = itemId.toUpperCase().trim()
                .replace("MINECRAFT:", "").replace("MC:", "");
            String materialName = numericIdToMaterial(clean);
            if (materialName == null) materialName = clean;
            if (materialName.equalsIgnoreCase("VIP") || materialName.startsWith("VIP"))
                return null;
            Material material = Material.matchMaterial(materialName);
            if (material != null && material.isItem()) return new ItemStack(material, 1);
            return null;
        } catch (Exception e) {
            getLogger().log(Level.WARNING, "parseItem Fehler: " + itemId, e);
            return null;
        }
    }

    private String numericIdToMaterial(String id) {
        if (!id.matches("\\d+")) return null;
        switch (id) {
            case "352": return "BONE_MEAL";
            case "264": return "DIAMOND";
            case "388": return "EMERALD";
            case "357": return "COOKIE";
            case "322": return "GOLDEN_CARROT";
            case "348": return "GOLD_INGOT";
            case "133": return "EMERALD_BLOCK";
            case "41":  return "GOLD_BLOCK";
            case "14":  return "GOLD_ORE";
            case "289": return "GUNPOWDER";
            case "1":   return "STONE";
            case "5":   return "OAK_PLANKS";
            case "17":  return "OAK_LOG";
            case "260": return "APPLE";
            case "287": return "STRING";
            default:    return null;
        }
    }

    // ===========================================================
    // FLY TRANSFER
    // ===========================================================

    private void executeTransfer(Player player, String code, String targetName) {
        // Bukkit.getPlayer() MUSS auf dem Main-Thread aufgerufen werden
        Player targetOnline = Bukkit.getPlayer(targetName);
        String resolvedName = (targetOnline != null) ? targetOnline.getName() : targetName;
        new BukkitRunnable() {
            @Override public void run() {
                boolean ok = flyCodeManager.transferCode(code, player.getName(), resolvedName);
                Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
                    if (ok) {
                        player.sendMessage(ChatColor.GREEN  + "✅ Gutschein "
                            + ChatColor.AQUA   + ChatColor.BOLD + code
                            + ChatColor.GREEN  + " wurde an "
                            + ChatColor.YELLOW + resolvedName
                            + ChatColor.GREEN  + " übertragen!");
                        if (targetOnline != null && targetOnline.isOnline()) {
                            targetOnline.sendMessage(
                                ChatColor.GOLD   + "✈ ════════════════════════════");
                            targetOnline.sendMessage(
                                ChatColor.YELLOW + "  Du hast einen Fly-Gutschein erhalten!");
                            targetOnline.sendMessage(
                                ChatColor.GRAY   + "  Von: "
                                + ChatColor.WHITE + player.getName());
                            targetOnline.sendMessage(
                                ChatColor.GRAY + "  Code: "
                                + ChatColor.AQUA + ChatColor.BOLD + code);
                            targetOnline.sendMessage(
                                ChatColor.GRAY  + "  Einlösen: "
                                + ChatColor.WHITE + "/flyredeem " + code);
                            targetOnline.sendMessage(
                                ChatColor.GOLD + "  ════════════════════════════");
                            targetOnline.playSound(targetOnline.getLocation(),
                                Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 1.2F);
                        }
                    } else {
                        player.sendMessage(ChatColor.RED
                            + "❌ Übertragung fehlgeschlagen. "
                            + "Spielernamen prüfen oder Code bereits eingelöst.");
                    }
                });
            }
        }.runTaskAsynchronously(this);
    }

    // ===========================================================
    // FLY MANAGER
    // ===========================================================

    private class FlyManager {

        private final IngameShopSpigot      plugin;
        private final Map<UUID, Integer>    flySeconds   = new HashMap<>();
        private final Map<UUID, BukkitTask> flyTasks     = new HashMap<>();
        private final Map<UUID, BossBar>    flyBossBars  = new HashMap<>();
        private final Map<UUID, Integer>    flyStartSecs = new HashMap<>();
        private final java.util.Set<UUID>   paused       = new java.util.HashSet<>();
        private final Map<UUID, Integer>    savedSeconds = new HashMap<>();

        FlyManager(IngameShopSpigot plugin) { this.plugin = plugin; }

        void grantFly(Player player, int seconds) {
            UUID uuid     = player.getUniqueId();
            int  existing = flySeconds.getOrDefault(uuid, 0);
            int  total    = existing + seconds;
            flySeconds.put(uuid, total);
            flyStartSecs.put(uuid, total);
            paused.remove(uuid);

            player.setAllowFlight(true);
            player.setFlying(true);
            player.sendMessage(ChatColor.AQUA + "✈ Fly aktiviert! Verbleibend: "
                + ChatColor.YELLOW + formatTime(total));

            BossBar bar = flyBossBars.get(uuid);
            if (bar == null) {
                bar = Bukkit.createBossBar(
                    buildBarTitle(total, false), BarColor.BLUE, BarStyle.SEGMENTED_10);
                bar.addPlayer(player);
                flyBossBars.put(uuid, bar);
            } else {
                bar.setTitle(buildBarTitle(total, false));
                bar.setProgress(1.0);
                bar.setColor(BarColor.BLUE);
            }

            if (flyTasks.containsKey(uuid)) flyTasks.get(uuid).cancel();

            final BossBar finalBar = bar;

            BukkitTask t = new BukkitRunnable() {
                @Override public void run() {
                    Player p = Bukkit.getPlayer(uuid);
                    if (p == null || !p.isOnline()) {
                        removeBossBar(uuid);
                        flyTasks.remove(uuid);
                        paused.remove(uuid);
                        cancel();
                        return;
                    }
                    if (paused.contains(uuid)) {
                        int rem = flySeconds.getOrDefault(uuid, 0);
                        finalBar.setTitle(buildBarTitle(rem, true));
                        finalBar.setColor(BarColor.WHITE);
                        return;
                    }
                    int remaining = flySeconds.getOrDefault(uuid, 0) - 1;
                    if (remaining <= 0) {
                        flySeconds.remove(uuid);
                        flyTasks.remove(uuid);
                        flyStartSecs.remove(uuid);
                        removeBossBar(uuid);
                        p.setFlying(false);
                        p.setAllowFlight(false);
                        p.sendMessage(ChatColor.RED + "✈ Fly-Zeit abgelaufen!");
                        p.playSound(p.getLocation(),
                            Sound.BLOCK_NOTE_BLOCK_BASS, 1.0F, 0.5F);
                        cancel();
                        return;
                    }
                    flySeconds.put(uuid, remaining);
                    int    start    = flyStartSecs.getOrDefault(uuid, remaining);
                    double progress = Math.max(0.0, Math.min(1.0,
                        (double) remaining / start));
                    finalBar.setProgress(progress);
                    finalBar.setTitle(buildBarTitle(remaining, false));
                    if (remaining <= 30)      finalBar.setColor(BarColor.RED);
                    else if (remaining <= 60) finalBar.setColor(BarColor.YELLOW);
                    else                      finalBar.setColor(BarColor.BLUE);
                    if (remaining == 300 || remaining == 60
                            || remaining == 30 || remaining == 10) {
                        p.sendMessage(ChatColor.YELLOW
                            + "✈ Fly noch " + formatTime(remaining) + "!");
                        p.playSound(p.getLocation(),
                            Sound.BLOCK_NOTE_BLOCK_PLING, 0.5F, 1.2F);
                    }
                }
            }.runTaskTimer(plugin, 20L, 20L);
            flyTasks.put(uuid, t);
        }

        boolean togglePause(Player player) {
            UUID    uuid = player.getUniqueId();
            BossBar bar  = flyBossBars.get(uuid);
            if (paused.contains(uuid)) {
                paused.remove(uuid);
                player.setAllowFlight(true);
                player.setFlying(true);
                if (bar != null) {
                    int rem = flySeconds.getOrDefault(uuid, 0);
                    bar.setTitle(buildBarTitle(rem, false));
                    bar.setColor(rem <= 30 ? BarColor.RED
                        : rem <= 60 ? BarColor.YELLOW : BarColor.BLUE);
                }
                return false;
            } else {
                paused.add(uuid);
                player.setFlying(false);
                player.setAllowFlight(false);
                if (bar != null) {
                    bar.setTitle(buildBarTitle(
                        flySeconds.getOrDefault(uuid, 0), true));
                    bar.setColor(BarColor.WHITE);
                }
                return true;
            }
        }

        private String buildBarTitle(int seconds, boolean isPaused) {
            if (isPaused)
                return ChatColor.GRAY + "⏸ Fly pausiert: "
                    + ChatColor.WHITE + formatTime(seconds);
            return ChatColor.AQUA + "✈ Fly-Zeit: "
                + ChatColor.YELLOW + formatTime(seconds);
        }

        private void removeBossBar(UUID uuid) {
            BossBar bar = flyBossBars.remove(uuid);
            if (bar != null) bar.removeAll();
        }

        int getRemainingSeconds(UUID uuid) {
            return flySeconds.getOrDefault(uuid, 0);
        }

        void saveOnQuit(Player player) {
            UUID uuid = player.getUniqueId();
            int  rem  = flySeconds.getOrDefault(uuid, 0);
            BukkitTask t = flyTasks.remove(uuid);
            if (t != null) t.cancel();
            removeBossBar(uuid);
            flySeconds.remove(uuid);
            flyStartSecs.remove(uuid);
            paused.remove(uuid);
            savedSeconds.remove(uuid);
            // Synchron speichern – beim Server-Stop werden async Tasks nicht mehr ausgeführt
            if (rem > 0) {
                flyCodeManager.saveFlySession(uuid, player.getName(), rem);
            }
        }

        void restoreOnJoin(Player player) {
            UUID uuid   = player.getUniqueId();
            int  ramRem = savedSeconds.remove(uuid) != null
                ? savedSeconds.getOrDefault(uuid, 0) : 0;
            if (ramRem > 0) {
                Bukkit.getScheduler().runTaskLater(plugin, () -> {
                    grantFly(player, ramRem);
                    togglePause(player);
                    player.sendMessage(ChatColor.AQUA
                        + "✈ Deine Fly-Zeit wurde wiederhergestellt: "
                        + ChatColor.YELLOW + formatTime(ramRem));
                    player.sendMessage(ChatColor.GRAY
                        + "  Fly ist pausiert. Fortsetzen: "
                        + ChatColor.WHITE + "/flypause");
                }, 20L);
                return;
            }
            new BukkitRunnable() {
                @Override public void run() {
                    int rem = flyCodeManager.loadAndDeleteFlySession(uuid);
                    if (rem > 0) {
                        Bukkit.getScheduler().runTaskLater(plugin, () -> {
                            if (player.isOnline()) {
                                grantFly(player, rem);
                                togglePause(player);
                                player.sendMessage(ChatColor.AQUA
                                    + "✈ Deine Fly-Zeit wurde wiederhergestellt: "
                                    + ChatColor.YELLOW + formatTime(rem));
                                player.sendMessage(ChatColor.GRAY
                                    + "  Fly ist pausiert. Fortsetzen: "
                                    + ChatColor.WHITE + "/flypause");
                            }
                        }, 20L);
                    }
                }
            }.runTaskAsynchronously(plugin);
        }

        void cancelAll() {
            for (Map.Entry<UUID, Integer> entry : flySeconds.entrySet()) {
                UUID   uuid = entry.getKey();
                int    rem  = entry.getValue();
                Player p    = Bukkit.getPlayer(uuid);
                // Name aus OfflinePlayer holen falls Spieler schon disconnected
                String name;
                if (p != null) {
                    name = p.getName();
                } else {
                    org.bukkit.OfflinePlayer op = Bukkit.getOfflinePlayer(uuid);
                    name = (op.getName() != null) ? op.getName() : uuid.toString();
                }
                // Synchron speichern als letzte Absicherung beim Server-Stop
                if (rem > 0) flyCodeManager.saveFlySession(uuid, name, rem);
                if (p != null && !p.isOp()) {
                    p.setFlying(false);
                    p.setAllowFlight(false);
                }
            }
            for (BukkitTask bt : flyTasks.values()) bt.cancel();
            for (BossBar   bar : flyBossBars.values()) bar.removeAll();
            flySeconds.clear();
            flyTasks.clear();
            flyBossBars.clear();
            flyStartSecs.clear();
            paused.clear();
            savedSeconds.clear();
        }

        /**
         * Speichert alle aktiven Fly-Sessions alle 30 Sekunden in die DB.
         * Schützt vor Datenverlust bei Absturz / Kill des Servers.
         */
        void startSessionPersist() {
            new BukkitRunnable() {
                @Override public void run() {
                    if (flySeconds.isEmpty()) return;
                    for (Map.Entry<UUID, Integer> entry : flySeconds.entrySet()) {
                        UUID uuid = entry.getKey();
                        int  rem  = entry.getValue();
                        if (rem <= 0) continue;
                        Player p = Bukkit.getPlayer(uuid);
                        String name = (p != null) ? p.getName()
                            : Bukkit.getOfflinePlayer(uuid).getName();
                        if (name == null) name = uuid.toString();
                        final String finalName = name;
                        new BukkitRunnable() {
                            @Override public void run() {
                                flyCodeManager.saveFlySession(uuid, finalName, rem);
                            }
                        }.runTaskAsynchronously(plugin);
                    }
                }
            }.runTaskTimer(plugin, 20L * 30, 20L * 30); // alle 30 Sekunden
        }

        String formatTime(int seconds) {
            if (seconds >= 3600) {
                int h = seconds / 3600, m = (seconds % 3600) / 60;
                return h + "h" + (m > 0 ? " " + m + "min" : "");
            } else if (seconds >= 60) {
                int m = seconds / 60, s = seconds % 60;
                return m + "min" + (s > 0 ? " " + s + "s" : "");
            }
            return seconds + "s";
        }

        /**
         * Wie grantFly, aber schreibt jede Sekunde die Nutzung in die Abo-Usage-Tabelle.
         * Stoppt automatisch wenn dailyLimitSec gesamt verbraucht ist.
         */
        void grantFlyWithAboTracking(Player player, int seconds, String aboLabel,
                                      String playerName, int dailyLimitSec) {
            UUID uuid     = player.getUniqueId();
            int  existing = flySeconds.getOrDefault(uuid, 0);
            int  total    = existing + seconds;
            flySeconds.put(uuid, total);
            flyStartSecs.put(uuid, total);
            paused.remove(uuid);

            player.setAllowFlight(true);
            player.setFlying(true);
            player.sendMessage(ChatColor.AQUA + "✈ Fly-Abo aktiv! Heute noch: "
                + ChatColor.YELLOW + formatTime(seconds));

            BossBar bar = flyBossBars.get(uuid);
            if (bar == null) {
                bar = Bukkit.createBossBar(
                    buildAboBarTitle(seconds, dailyLimitSec, false),
                    BarColor.GREEN, BarStyle.SEGMENTED_10);
                bar.addPlayer(player);
                flyBossBars.put(uuid, bar);
            } else {
                bar.setTitle(buildAboBarTitle(seconds, dailyLimitSec, false));
                bar.setProgress(1.0);
                bar.setColor(BarColor.GREEN);
            }

            if (flyTasks.containsKey(uuid)) flyTasks.get(uuid).cancel();

            final BossBar finalBar = bar;
            // Jede 30s Usage in DB schreiben
            final int[] dbBuffer = {0};

            BukkitTask t = new BukkitRunnable() {
                @Override public void run() {
                    Player p = Bukkit.getPlayer(uuid);
                    if (p == null || !p.isOnline()) {
                        // DB-Puffer flushen
                        if (dbBuffer[0] > 0) {
                            final int toFlush = dbBuffer[0];
                            dbBuffer[0] = 0;
                            new BukkitRunnable() {
                                @Override public void run() {
                                    flyCodeManager.addUsedTodaySec(playerName, toFlush);
                                }
                            }.runTaskAsynchronously(plugin);
                        }
                        removeBossBar(uuid);
                        flyTasks.remove(uuid);
                        paused.remove(uuid);
                        cancel();
                        return;
                    }
                    if (paused.contains(uuid)) {
                        int rem = flySeconds.getOrDefault(uuid, 0);
                        finalBar.setTitle(buildAboBarTitle(rem, dailyLimitSec, true));
                        finalBar.setColor(BarColor.WHITE);
                        return;
                    }
                    int remaining = flySeconds.getOrDefault(uuid, 0) - 1;
                    dbBuffer[0]++;
                    // Alle 30s in DB flushen
                    if (dbBuffer[0] >= 30) {
                        final int toFlush = dbBuffer[0];
                        dbBuffer[0] = 0;
                        new BukkitRunnable() {
                            @Override public void run() {
                                flyCodeManager.addUsedTodaySec(playerName, toFlush);
                            }
                        }.runTaskAsynchronously(plugin);
                    }
                    if (remaining <= 0) {
                        // Letzten Rest flushen
                        if (dbBuffer[0] > 0) {
                            final int toFlush = dbBuffer[0];
                            new BukkitRunnable() {
                                @Override public void run() {
                                    flyCodeManager.addUsedTodaySec(playerName, toFlush);
                                }
                            }.runTaskAsynchronously(plugin);
                        }
                        flySeconds.remove(uuid);
                        flyTasks.remove(uuid);
                        flyStartSecs.remove(uuid);
                        removeBossBar(uuid);
                        p.setFlying(false);
                        p.setAllowFlight(false);
                        p.sendMessage(ChatColor.RED    + "✈ Fly-Abo Tageslimit erreicht!");
                        p.sendMessage(ChatColor.GRAY   + "  Morgen stehen dir wieder "
                            + ChatColor.WHITE + formatTime(dailyLimitSec)
                            + ChatColor.GRAY  + " zur Verfügung.");
                        p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1.0F, 0.5F);
                        cancel();
                        return;
                    }
                    flySeconds.put(uuid, remaining);
                    int    start    = flyStartSecs.getOrDefault(uuid, remaining);
                    double progress = Math.max(0.0, Math.min(1.0, (double) remaining / start));
                    finalBar.setProgress(progress);
                    finalBar.setTitle(buildAboBarTitle(remaining, dailyLimitSec, false));
                    if (remaining <= 300)       finalBar.setColor(BarColor.RED);
                    else if (remaining <= 1800) finalBar.setColor(BarColor.YELLOW);
                    else                        finalBar.setColor(BarColor.GREEN);
                    if (remaining == 3600 || remaining == 1800
                            || remaining == 600 || remaining == 300
                            || remaining == 60  || remaining == 30 || remaining == 10) {
                        p.sendMessage(ChatColor.YELLOW + "✈ Fly-Abo noch " + formatTime(remaining) + " heute!");
                        p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 0.5F, 1.2F);
                    }
                }
            }.runTaskTimer(plugin, 20L, 20L);
            flyTasks.put(uuid, t);
        }

        private String buildAboBarTitle(int seconds, int dailyMax, boolean isPaused) {
            if (isPaused)
                return ChatColor.GRAY + "⏸ Fly-Abo pausiert: "
                    + ChatColor.WHITE + formatTime(seconds);
            return ChatColor.GREEN + "✈ Fly-Abo: "
                + ChatColor.YELLOW + formatTime(seconds)
                + ChatColor.GRAY   + " / " + formatTime(dailyMax);
        }
    }

    // ===========================================================
    // FLY CODE MANAGER
    // ===========================================================

    private class FlyCodeManager {

        private final IngameShopSpigot plugin;
        private       Connection       connection;
        private final String           dbHost, dbPort, dbName, dbUser, dbPass;

        private static final String CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
        private final SecureRandom  rng   = new SecureRandom();

        FlyCodeManager(IngameShopSpigot plugin) {
            this.plugin = plugin;
            dbHost = plugin.getConfig().getString("mysql.host",     "localhost");
            dbPort = plugin.getConfig().getString("mysql.port",     "3306");
            dbName = plugin.getConfig().getString("mysql.database", "minecraft");
            dbUser = plugin.getConfig().getString("mysql.username", "root");
            dbPass = plugin.getConfig().getString("mysql.password", "");
        }

        boolean connect() {
            try {
                Class.forName("com.mysql.cj.jdbc.Driver");
                String url = "jdbc:mysql://" + dbHost + ":" + dbPort + "/" + dbName
                    + "?useSSL=false&autoReconnect=true&characterEncoding=utf8";
                connection = DriverManager.getConnection(url, dbUser, dbPass);
                return true;
            } catch (Exception e) {
                plugin.getLogger().log(Level.SEVERE, "MySQL Verbindungsfehler", e);
                return false;
            }
        }

        void disconnect() {
            try {
                if (connection != null && !connection.isClosed()) connection.close();
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "MySQL Disconnect Fehler", e);
            }
        }

        void createTable() {
            String sqlCodes =
                "CREATE TABLE IF NOT EXISTS wis_fly_codes ("
                + "  id           INT AUTO_INCREMENT PRIMARY KEY,"
                + "  code         VARCHAR(20)  NOT NULL UNIQUE,"
                + "  player_name  VARCHAR(64)  NOT NULL COLLATE utf8mb4_general_ci,"
                + "  duration_sec INT          NOT NULL,"
                + "  label        VARCHAR(64)  NOT NULL,"
                + "  used         TINYINT(1)   NOT NULL DEFAULT 0,"
                + "  redeemed_by  VARCHAR(64)  DEFAULT NULL COLLATE utf8mb4_general_ci,"
                + "  created_at   DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,"
                + "  redeemed_at  DATETIME     DEFAULT NULL"
                + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
            String sqlSessions =
                "CREATE TABLE IF NOT EXISTS wis_fly_sessions ("
                + "  uuid          VARCHAR(36) NOT NULL PRIMARY KEY,"
                + "  player_name   VARCHAR(64) NOT NULL COLLATE utf8mb4_general_ci,"
                + "  remaining_sec INT         NOT NULL,"
                + "  saved_at      DATETIME    NOT NULL DEFAULT CURRENT_TIMESTAMP"
                + "                ON UPDATE CURRENT_TIMESTAMP"
                + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
            String sqlRanks =
                "CREATE TABLE IF NOT EXISTS wis_rank_sessions ("
                + "  id            INT AUTO_INCREMENT PRIMARY KEY,"
                + "  player_name   VARCHAR(64) NOT NULL COLLATE utf8mb4_general_ci,"
                + "  rank_id       VARCHAR(64) NOT NULL,"
                + "  lp_group      VARCHAR(64) NOT NULL,"
                + "  default_group VARCHAR(64) NOT NULL DEFAULT 'default',"
                + "  permanent     TINYINT(1)  NOT NULL DEFAULT 0,"
                + "  expires_at    DATETIME    DEFAULT NULL,"
                + "  granted_at    DATETIME    NOT NULL DEFAULT CURRENT_TIMESTAMP,"
                + "  UNIQUE KEY uq_player_rank (player_name, rank_id)"
                + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
            String sqlRanksMigrate =
                "ALTER TABLE wis_rank_sessions"
                + " ADD COLUMN IF NOT EXISTS default_group VARCHAR(64) NOT NULL DEFAULT 'default'"
                + " AFTER lp_group;";
            try (Statement stmt = connection.createStatement()) {
                stmt.execute(sqlCodes);
                stmt.execute(sqlSessions);
                stmt.execute(sqlRanks);
                // Migration: default_group-Spalte ergänzen falls noch nicht vorhanden
                try { stmt.execute(sqlRanksMigrate); } catch (SQLException ignored) {}
            } catch (SQLException e) {
                plugin.getLogger().log(Level.SEVERE, "Tabellen-Erstellung fehlgeschlagen", e);
            }
        }

        void saveFlySession(UUID uuid, String playerName, int remainingSec) {
            String sql =
                "INSERT INTO wis_fly_sessions (uuid, player_name, remaining_sec) "
                + "VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE "
                + "player_name = ?, remaining_sec = ?, saved_at = NOW()";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, uuid.toString());
                ps.setString(2, playerName);
                ps.setInt(3, remainingSec);
                ps.setString(4, playerName);
                ps.setInt(5, remainingSec);
                ps.executeUpdate();
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "saveFlySession SQL Fehler", e);
            }
        }

        int loadAndDeleteFlySession(UUID uuid) {
            String selectSql = "SELECT remaining_sec FROM wis_fly_sessions WHERE uuid = ?";
            String deleteSql = "DELETE FROM wis_fly_sessions WHERE uuid = ?";
            try {
                int remaining = -1;
                try (PreparedStatement ps = connection.prepareStatement(selectSql)) {
                    ps.setString(1, uuid.toString());
                    try (ResultSet rs = ps.executeQuery()) {
                        if (rs.next()) remaining = rs.getInt("remaining_sec");
                    }
                }
                if (remaining > 0) {
                    try (PreparedStatement ps = connection.prepareStatement(deleteSql)) {
                        ps.setString(1, uuid.toString());
                        ps.executeUpdate();
                    }
                }
                return remaining;
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "loadFlySession SQL Fehler", e);
                return -1;
            }
        }

        void deleteFlySession(UUID uuid) {
            String sql = "DELETE FROM wis_fly_sessions WHERE uuid = ?";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, uuid.toString());
                ps.executeUpdate();
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "deleteFlySession SQL Fehler", e);
            }
        }

        // ── Rang-Sessions ────────────────────────────────────────────────

        void saveRankSession(String playerName, String rankId, String group,
                             String defaultGroup, int days) {
            String sql = days == 0
                ? "INSERT INTO wis_rank_sessions (player_name, rank_id, lp_group, default_group, permanent, expires_at)"
                  + " VALUES (?, ?, ?, ?, 1, NULL)"
                  + " ON DUPLICATE KEY UPDATE lp_group=?, default_group=?, permanent=1, expires_at=NULL, granted_at=NOW()"
                : "INSERT INTO wis_rank_sessions (player_name, rank_id, lp_group, default_group, permanent, expires_at)"
                  + " VALUES (?, ?, ?, ?, 0, DATE_ADD(NOW(), INTERVAL ? DAY))"
                  + " ON DUPLICATE KEY UPDATE lp_group=?, default_group=?, permanent=0,"
                  + " expires_at=DATE_ADD(NOW(), INTERVAL ? DAY), granted_at=NOW()";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, playerName);
                ps.setString(2, rankId);
                ps.setString(3, group);
                ps.setString(4, defaultGroup);
                if (days == 0) {
                    ps.setString(5, group);
                    ps.setString(6, defaultGroup);
                } else {
                    ps.setInt(5, days);
                    ps.setString(6, group);
                    ps.setString(7, defaultGroup);
                    ps.setInt(8, days);
                }
                ps.executeUpdate();
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "saveRankSession SQL Fehler", e);
            }
        }

        List<RankManager.RankEntry> getExpiredRanks(String playerName) {
            List<RankManager.RankEntry> list = new ArrayList<>();
            String sql =
                "SELECT id, rank_id, lp_group, default_group,"
                + " DATE_FORMAT(expires_at, '%d.%m.%Y') AS expires_fmt"
                + " FROM wis_rank_sessions"
                + " WHERE player_name = ? AND permanent = 0 AND expires_at <= NOW()";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, playerName);
                try (ResultSet rs = ps.executeQuery()) {
                    while (rs.next()) {
                        String dg = rs.getString("default_group");
                        list.add(new RankManager.RankEntry(
                            rs.getInt("id"),
                            rs.getString("rank_id"),
                            rs.getString("lp_group"),
                            dg != null ? dg : "default",
                            rs.getString("expires_fmt"),
                            false));
                    }
                }
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "getExpiredRanks SQL Fehler", e);
            }
            return list;
        }

        List<RankManager.RankEntry> getActiveRanks(String playerName) {
            List<RankManager.RankEntry> list = new ArrayList<>();
            String sql =
                "SELECT id, rank_id, lp_group, default_group, permanent,"
                + " DATE_FORMAT(expires_at, '%d.%m.%Y %H:%i') AS expires_fmt"
                + " FROM wis_rank_sessions"
                + " WHERE player_name = ? AND (permanent = 1 OR expires_at > NOW())"
                + " ORDER BY granted_at DESC";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, playerName);
                try (ResultSet rs = ps.executeQuery()) {
                    while (rs.next()) {
                        String dg = rs.getString("default_group");
                        list.add(new RankManager.RankEntry(
                            rs.getInt("id"),
                            rs.getString("rank_id"),
                            rs.getString("lp_group"),
                            dg != null ? dg : "default",
                            rs.getString("expires_fmt") != null
                                ? rs.getString("expires_fmt") : "–",
                            rs.getInt("permanent") == 1));
                    }
                }
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "getActiveRanks SQL Fehler", e);
            }
            return list;
        }

        void deleteRankSession(int id) {
            String sql = "DELETE FROM wis_rank_sessions WHERE id = ?";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setInt(1, id);
                ps.executeUpdate();
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "deleteRankSession SQL Fehler", e);
            }
        }

        // ── Fly-Abo CRUD ──────────────────────────────────────────────────

        void createAboTable() {
            // Migration: alte expires_at-Tabelle auf neues Schema upgraden
            String sqlAbo =
                "CREATE TABLE IF NOT EXISTS wis_fly_abos ("
                + "  id                  INT AUTO_INCREMENT PRIMARY KEY,"
                + "  player_name         VARCHAR(64) NOT NULL COLLATE utf8mb4_general_ci,"
                + "  label               VARCHAR(128) NOT NULL,"
                + "  monthly_price       INT NOT NULL DEFAULT 0,"
                + "  cancelled           TINYINT(1) NOT NULL DEFAULT 0,"
                + "  cancellation_reason VARCHAR(32) DEFAULT NULL,"
                + "  next_billing_date   DATE NOT NULL,"
                + "  period_end          DATE NOT NULL,"
                + "  granted_at          DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,"
                + "  UNIQUE KEY uq_player (player_name)"
                + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
            // Migrations: Spalten ergänzen falls Tabelle schon in alter Form existiert
            String[] migrations = {
                "ALTER TABLE wis_fly_abos ADD COLUMN IF NOT EXISTS monthly_price INT NOT NULL DEFAULT 0 AFTER label",
                "ALTER TABLE wis_fly_abos ADD COLUMN IF NOT EXISTS cancellation_reason VARCHAR(32) DEFAULT NULL AFTER cancelled",
                // expires_at Spalte entfernen falls vorhanden (altes Schema – verhindert INSERT)
                "ALTER TABLE wis_fly_abos DROP COLUMN IF EXISTS expires_at",
                "ALTER TABLE wis_fly_abos ADD COLUMN IF NOT EXISTS next_billing_date DATE NOT NULL DEFAULT (CURDATE()) AFTER cancellation_reason",
                "ALTER TABLE wis_fly_abos ADD COLUMN IF NOT EXISTS period_end DATE NOT NULL DEFAULT (LAST_DAY(CURDATE())) AFTER next_billing_date",
                // Alte Einträge mit period_end = 0000-00-00 reparieren
                "UPDATE wis_fly_abos SET"
                + "  period_end = LAST_DAY(CURDATE()),"
                + "  next_billing_date = DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01')"
                + " WHERE period_end = '0000-00-00' OR period_end IS NULL OR period_end < '2020-01-01'",
            };
            String sqlUsage =
                "CREATE TABLE IF NOT EXISTS wis_fly_abo_usage ("
                + "  id            INT AUTO_INCREMENT PRIMARY KEY,"
                + "  player_name   VARCHAR(64) NOT NULL COLLATE utf8mb4_general_ci,"
                + "  usage_date    DATE NOT NULL,"
                + "  used_sec      INT NOT NULL DEFAULT 0,"
                + "  UNIQUE KEY uq_player_date (player_name, usage_date)"
                + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
            try (Statement stmt = connection.createStatement()) {
                stmt.execute(sqlAbo);
                stmt.execute(sqlUsage);
                for (String m : migrations) {
                    try { stmt.execute(m); } catch (SQLException ignored) {}
                }
            } catch (SQLException e) {
                plugin.getLogger().log(Level.SEVERE, "Abo-Tabellen-Erstellung fehlgeschlagen", e);
            }
        }

        /**
         * Neues Abo aktivieren oder bestehendes verlängern.
         * next_billing_date = 1. des Folgemonats
         * period_end        = letzter Tag des laufenden Monats (oder des aktuellen Abomonats)
         */
        void saveAbo(String playerName, String label, int monthlyPrice) {
            String sql =
                "INSERT INTO wis_fly_abos"
                + " (player_name, label, monthly_price, cancelled, cancellation_reason,"
                + "  next_billing_date, period_end)"
                + " VALUES (?, ?, ?, 0, NULL,"
                + "  DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01'),"
                + "  LAST_DAY(CURDATE()))"
                + " ON DUPLICATE KEY UPDATE"
                + "   label = ?, monthly_price = ?, cancelled = 0, cancellation_reason = NULL,"
                + "   next_billing_date = DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01'),"
                + "   period_end = LAST_DAY(CURDATE()),"
                + "   granted_at = NOW()";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, playerName);
                ps.setString(2, label);
                ps.setInt(3, monthlyPrice);
                ps.setString(4, label);
                ps.setInt(5, monthlyPrice);
                ps.executeUpdate();
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "saveAbo SQL Fehler", e);
            }
        }

        /**
         * Abo durch Spieler kündigen – bleibt bis period_end aktiv.
         * period_end wird auf LAST_DAY(CURDATE()) gesetzt (Ende des laufenden Monats).
         */
        boolean cancelAbo(String playerName) {
            String sql =
                "UPDATE wis_fly_abos SET cancelled = 1, cancellation_reason = 'user',"
                + " period_end = LAST_DAY(CURDATE())"
                + " WHERE player_name = ? AND period_end >= CURDATE() AND cancelled = 0";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, playerName);
                return ps.executeUpdate() == 1;
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "cancelAbo SQL Fehler", e);
                return false;
            }
        }

        /**
         * Abo wegen Zahlungsausfall kündigen – sofort zum period_end des laufenden Monats.
         */
        void cancelAboPaymentFailed(String playerName) {
            String sql =
                "UPDATE wis_fly_abos SET cancelled = 1, cancellation_reason = 'payment_failed',"
                + " period_end = LAST_DAY(CURDATE())"
                + " WHERE player_name = ? AND cancelled = 0";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, playerName);
                ps.executeUpdate();
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "cancelAboPaymentFailed SQL Fehler", e);
            }
        }

        /**
         * Billing verlängern: next_billing_date auf 1. des übernächsten Monats,
         * period_end auf letzten Tag des neuen Monats setzen.
         */
        void renewAbo(String playerName) {
            String sql =
                "UPDATE wis_fly_abos"
                + " SET next_billing_date = DATE_FORMAT(DATE_ADD(next_billing_date, INTERVAL 1 MONTH), '%Y-%m-01'),"
                + "     period_end = LAST_DAY(next_billing_date)"
                + " WHERE player_name = ? AND cancelled = 0";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, playerName);
                ps.executeUpdate();
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "renewAbo SQL Fehler", e);
            }
        }

        /**
         * Alle Abos holen die heute fällig sind (next_billing_date = heute, nicht gekündigt).
         */
        List<FlyAboManager.AboEntry> getDueBillings() {
            List<FlyAboManager.AboEntry> list = new ArrayList<>();
            String sql =
                "SELECT player_name, label, monthly_price, cancelled, cancellation_reason,"
                + " DATE_FORMAT(next_billing_date, '%d.%m.%Y') AS billing_fmt,"
                + " DATE_FORMAT(period_end, '%d.%m.%Y') AS period_fmt"
                + " FROM wis_fly_abos"
                + " WHERE next_billing_date <= CURDATE() AND cancelled = 0";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                try (ResultSet rs = ps.executeQuery()) {
                    while (rs.next())
                        list.add(new FlyAboManager.AboEntry(
                            rs.getString("player_name"),
                            rs.getString("label"),
                            rs.getInt("monthly_price"),
                            false,
                            null,
                            rs.getString("billing_fmt"),
                            rs.getString("period_fmt")));
                }
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "getDueBillings SQL Fehler", e);
            }
            return list;
        }

        FlyAboManager.AboEntry getActiveAbo(String playerName) {
            String sql =
                "SELECT player_name, label, monthly_price, cancelled, cancellation_reason,"
                + " DATE_FORMAT(next_billing_date, '%d.%m.%Y') AS billing_fmt,"
                + " DATE_FORMAT(period_end, '%d.%m.%Y') AS period_fmt"
                + " FROM wis_fly_abos"
                + " WHERE player_name = ? AND period_end >= CURDATE()";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, playerName);
                try (ResultSet rs = ps.executeQuery()) {
                    if (rs.next())
                        return new FlyAboManager.AboEntry(
                            rs.getString("player_name"),
                            rs.getString("label"),
                            rs.getInt("monthly_price"),
                            rs.getInt("cancelled") == 1,
                            rs.getString("cancellation_reason"),
                            rs.getString("billing_fmt"),
                            rs.getString("period_fmt"));
                }
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "getActiveAbo SQL Fehler", e);
            }
            return null;
        }

        void cleanupExpiredAbos(String playerName) {
            String sql = "DELETE FROM wis_fly_abos WHERE player_name = ? AND period_end < CURDATE()";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, playerName);
                ps.executeUpdate();
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "cleanupExpiredAbos SQL Fehler", e);
            }
        }

        int getUsedTodaySec(String playerName) {
            String sql =
                "SELECT used_sec FROM wis_fly_abo_usage"
                + " WHERE player_name = ? AND usage_date = CURDATE()";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, playerName);
                try (ResultSet rs = ps.executeQuery()) {
                    if (rs.next()) return rs.getInt("used_sec");
                }
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "getUsedTodaySec SQL Fehler", e);
            }
            return 0;
        }

        void addUsedTodaySec(String playerName, int seconds) {
            String sql =
                "INSERT INTO wis_fly_abo_usage (player_name, usage_date, used_sec)"
                + " VALUES (?, CURDATE(), ?)"
                + " ON DUPLICATE KEY UPDATE used_sec = used_sec + ?";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, playerName);
                ps.setInt(2, seconds);
                ps.setInt(3, seconds);
                ps.executeUpdate();
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "addUsedTodaySec SQL Fehler", e);
            }
        }

        // ── Plot-Slot CRUD ────────────────────────────────────────────────

        void createPlotSlotTables() {
            String sqlExtra =
                "CREATE TABLE IF NOT EXISTS wis_plot_extra_slots ("
                + "  id          INT AUTO_INCREMENT PRIMARY KEY,"
                + "  player_name VARCHAR(64) NOT NULL COLLATE utf8mb4_general_ci,"
                + "  extra_slots INT NOT NULL DEFAULT 0,"
                + "  last_perm   INT NOT NULL DEFAULT 0,"
                + "  granted_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,"
                + "  UNIQUE KEY uq_player (player_name)"
                + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
            String sqlAbo =
                "CREATE TABLE IF NOT EXISTS wis_plot_abos ("
                + "  id                  INT AUTO_INCREMENT PRIMARY KEY,"
                + "  player_name         VARCHAR(64) NOT NULL COLLATE utf8mb4_general_ci,"
                + "  label               VARCHAR(128) NOT NULL,"
                + "  abo_slots           INT NOT NULL DEFAULT 1,"
                + "  monthly_price       INT NOT NULL DEFAULT 0,"
                + "  cancelled           TINYINT(1) NOT NULL DEFAULT 0,"
                + "  cancellation_reason VARCHAR(32) DEFAULT NULL,"
                + "  next_billing_date   DATE NOT NULL,"
                + "  period_end          DATE NOT NULL,"
                + "  granted_at          DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,"
                + "  UNIQUE KEY uq_player (player_name)"
                + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
            try (Statement stmt = connection.createStatement()) {
                stmt.execute(sqlExtra);
                stmt.execute(sqlAbo);
                // Migration: last_perm Spalte ergänzen falls Tabelle schon existiert
                try { stmt.execute("ALTER TABLE wis_plot_extra_slots ADD COLUMN IF NOT EXISTS last_perm INT NOT NULL DEFAULT 0 AFTER extra_slots"); }
                catch (SQLException ignored) {}
            } catch (SQLException e) {
                plugin.getLogger().log(Level.SEVERE, "Plot-Slot-Tabellen Erstellung fehlgeschlagen", e);
            }
        }

        // Letzten Permission-Wert lesen/schreiben (für sauberes Entfernen)
        int getLastPerm(String playerName) {
            String sql = "SELECT last_perm FROM wis_plot_extra_slots WHERE player_name = ?";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, playerName);
                try (ResultSet rs = ps.executeQuery()) {
                    if (rs.next()) return rs.getInt("last_perm");
                }
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "getLastPerm SQL Fehler", e);
            }
            return 0;
        }

        void saveLastPerm(String playerName, int perm) {
            String sql =
                "INSERT INTO wis_plot_extra_slots (player_name, extra_slots, last_perm)"
                + " VALUES (?, 0, ?)"
                + " ON DUPLICATE KEY UPDATE last_perm = ?";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, playerName);
                ps.setInt(2, perm);
                ps.setInt(3, perm);
                ps.executeUpdate();
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "saveLastPerm SQL Fehler", e);
            }
        }

        // Extra-Slots (permanent)
        void addExtraSlots(String playerName, int slots) {
            String sql =
                "INSERT INTO wis_plot_extra_slots (player_name, extra_slots)"
                + " VALUES (?, ?)"
                + " ON DUPLICATE KEY UPDATE extra_slots = extra_slots + ?";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, playerName);
                ps.setInt(2, slots);
                ps.setInt(3, slots);
                ps.executeUpdate();
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "addExtraSlots SQL Fehler", e);
            }
        }

        int getExtraSlots(String playerName) {
            String sql = "SELECT extra_slots FROM wis_plot_extra_slots WHERE player_name = ?";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, playerName);
                try (ResultSet rs = ps.executeQuery()) {
                    if (rs.next()) return rs.getInt("extra_slots");
                }
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "getExtraSlots SQL Fehler", e);
            }
            return 0;
        }

        // Plot-Abo
        void savePlotAbo(String playerName, int slots, String label, int monthlyPrice) {
            String sql =
                "INSERT INTO wis_plot_abos"
                + " (player_name, label, abo_slots, monthly_price, cancelled, cancellation_reason,"
                + "  next_billing_date, period_end)"
                + " VALUES (?, ?, ?, ?, 0, NULL,"
                + "  DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01'),"
                + "  LAST_DAY(CURDATE()))"
                + " ON DUPLICATE KEY UPDATE"
                + "   label = ?, abo_slots = ?, monthly_price = ?,"
                + "   cancelled = 0, cancellation_reason = NULL,"
                + "   next_billing_date = DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01'),"
                + "   period_end = LAST_DAY(CURDATE()), granted_at = NOW()";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, playerName); ps.setString(2, label);
                ps.setInt(3, slots);         ps.setInt(4, monthlyPrice);
                ps.setString(5, label);      ps.setInt(6, slots);
                ps.setInt(7, monthlyPrice);
                ps.executeUpdate();
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "savePlotAbo SQL Fehler", e);
            }
        }

        boolean cancelPlotAbo(String playerName) {
            String sql =
                "UPDATE wis_plot_abos SET cancelled = 1, cancellation_reason = 'user',"
                + " period_end = LAST_DAY(CURDATE())"
                + " WHERE player_name = ? AND period_end >= CURDATE() AND cancelled = 0";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, playerName);
                return ps.executeUpdate() == 1;
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "cancelPlotAbo SQL Fehler", e);
                return false;
            }
        }

        void cancelPlotAboPaymentFailed(String playerName) {
            String sql =
                "UPDATE wis_plot_abos SET cancelled = 1, cancellation_reason = 'payment_failed',"
                + " period_end = LAST_DAY(CURDATE())"
                + " WHERE player_name = ? AND cancelled = 0";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, playerName);
                ps.executeUpdate();
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "cancelPlotAboPaymentFailed SQL Fehler", e);
            }
        }

        void renewPlotAbo(String playerName) {
            String sql =
                "UPDATE wis_plot_abos"
                + " SET next_billing_date = DATE_FORMAT(DATE_ADD(next_billing_date, INTERVAL 1 MONTH), '%Y-%m-01'),"
                + "     period_end = LAST_DAY(next_billing_date)"
                + " WHERE player_name = ? AND cancelled = 0";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, playerName);
                ps.executeUpdate();
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "renewPlotAbo SQL Fehler", e);
            }
        }

        int getPlotAboSlots(String playerName) {
            String sql =
                "SELECT abo_slots FROM wis_plot_abos"
                + " WHERE player_name = ? AND period_end >= CURDATE() AND cancelled = 0";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, playerName);
                try (ResultSet rs = ps.executeQuery()) {
                    if (rs.next()) return rs.getInt("abo_slots");
                }
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "getPlotAboSlots SQL Fehler", e);
            }
            return 0;
        }

        PlotSlotManager.PlotAboEntry getActivePlotAbo(String playerName) {
            String sql =
                "SELECT label, abo_slots, monthly_price, cancelled, cancellation_reason,"
                + " DATE_FORMAT(next_billing_date, '%d.%m.%Y') AS billing_fmt,"
                + " DATE_FORMAT(period_end, '%d.%m.%Y') AS period_fmt"
                + " FROM wis_plot_abos"
                + " WHERE player_name = ? AND period_end >= CURDATE()";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, playerName);
                try (ResultSet rs = ps.executeQuery()) {
                    if (rs.next())
                        return new PlotSlotManager.PlotAboEntry(
                            playerName,
                            rs.getString("label"),
                            rs.getInt("abo_slots"),
                            rs.getInt("monthly_price"),
                            rs.getInt("cancelled") == 1,
                            rs.getString("cancellation_reason"),
                            rs.getString("billing_fmt"),
                            rs.getString("period_fmt"));
                }
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "getActivePlotAbo SQL Fehler", e);
            }
            return null;
        }

        List<PlotSlotManager.PlotAboEntry> getDuePlotBillings() {
            List<PlotSlotManager.PlotAboEntry> list = new ArrayList<>();
            String sql =
                "SELECT player_name, label, abo_slots, monthly_price,"
                + " DATE_FORMAT(next_billing_date, '%d.%m.%Y') AS billing_fmt,"
                + " DATE_FORMAT(period_end, '%d.%m.%Y') AS period_fmt"
                + " FROM wis_plot_abos"
                + " WHERE next_billing_date <= CURDATE() AND cancelled = 0";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                try (ResultSet rs = ps.executeQuery()) {
                    while (rs.next())
                        list.add(new PlotSlotManager.PlotAboEntry(
                            rs.getString("player_name"),
                            rs.getString("label"),
                            rs.getInt("abo_slots"),
                            rs.getInt("monthly_price"),
                            false, null,
                            rs.getString("billing_fmt"),
                            rs.getString("period_fmt")));
                }
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "getDuePlotBillings SQL Fehler", e);
            }
            return list;
        }

        // ── Fly-Code CRUD ────────────────────────────────────────────────

        String generateCode(String playerName, int durationSec, String label) {
            String code;
            int    attempts = 0;
            do {
                code = "FLY-" + randomSegment(4) + "-" + randomSegment(4);
                attempts++;
            } while (!insertCode(code, playerName, durationSec, label) && attempts < 20);
            return code;
        }

        private boolean insertCode(String code, String playerName,
                                    int durationSec, String label) {
            String sql =
                "INSERT INTO wis_fly_codes (code, player_name, duration_sec, label)"
                + " VALUES (?, ?, ?, ?)";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, code);
                ps.setString(2, playerName);
                ps.setInt(3, durationSec);
                ps.setString(4, label);
                ps.executeUpdate();
                return true;
            } catch (SQLException e) {
                if (debug) plugin.getLogger().log(
                    Level.WARNING, "Code-Insert Kollision: " + code, e);
                return false;
            }
        }

        int redeemCode(String code) {
            String checkSql  =
                "SELECT id, duration_sec, used FROM wis_fly_codes WHERE code = ?";
            String updateSql =
                "UPDATE wis_fly_codes SET used = 1, redeemed_at = NOW()"
                + " WHERE id = ? AND used = 0";
            try {
                int id = -1, durationSec = -1;
                try (PreparedStatement ps = connection.prepareStatement(checkSql)) {
                    ps.setString(1, code);
                    try (ResultSet rs = ps.executeQuery()) {
                        if (!rs.next())             return -1;
                        if (rs.getInt("used") == 1) return -1;
                        id          = rs.getInt("id");
                        durationSec = rs.getInt("duration_sec");
                    }
                }
                try (PreparedStatement ps = connection.prepareStatement(updateSql)) {
                    ps.setInt(1, id);
                    if (ps.executeUpdate() == 0) return -1;
                }
                return durationSec;
            } catch (SQLException e) {
                plugin.getLogger().log(Level.SEVERE, "redeemCode SQL Fehler", e);
                return -1;
            }
        }

        List<FlyCodeEntry> getUnusedCodes(String playerName) {
            List<FlyCodeEntry> list = new ArrayList<>();
            String sql =
                "SELECT code, label, duration_sec,"
                + " DATE_FORMAT(created_at, '%d.%m.%Y') AS created_at"
                + " FROM wis_fly_codes WHERE player_name = ? AND used = 0"
                + " ORDER BY created_at DESC";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, playerName);
                try (ResultSet rs = ps.executeQuery()) {
                    while (rs.next())
                        list.add(new FlyCodeEntry(
                            rs.getString("code"),
                            rs.getString("label"),
                            rs.getString("created_at"),
                            rs.getInt("duration_sec")));
                }
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "getUnusedCodes SQL Fehler", e);
            }
            return list;
        }

        boolean transferCode(String code, String fromPlayer, String toPlayer) {
            String sql =
                "UPDATE wis_fly_codes SET player_name = ?"
                + " WHERE code = ? AND player_name = ? AND used = 0";
            try (PreparedStatement ps = connection.prepareStatement(sql)) {
                ps.setString(1, toPlayer);
                ps.setString(2, code);
                ps.setString(3, fromPlayer);
                return ps.executeUpdate() == 1;
            } catch (SQLException e) {
                plugin.getLogger().log(Level.WARNING, "transferCode SQL Fehler", e);
                return false;
            }
        }

        private String randomSegment(int length) {
            StringBuilder sb = new StringBuilder(length);
            for (int i = 0; i < length; i++)
                sb.append(CHARS.charAt(rng.nextInt(CHARS.length())));
            return sb.toString();
        }

        static class FlyCodeEntry {
            final String code, label, createdAt;
            final int    durationSec;
            FlyCodeEntry(String code, String label, String createdAt, int durationSec) {
                this.code        = code;
                this.label       = label;
                this.createdAt   = createdAt;
                this.durationSec = durationSec;
            }
        }
    }

    // ===========================================================
    // RANK MANAGER
    // ===========================================================

    private class RankManager {

        private final IngameShopSpigot plugin;
        private final FlyCodeManager   db;

        RankManager(IngameShopSpigot plugin, FlyCodeManager db) {
            this.plugin = plugin;
            this.db     = db;
        }

        void grantRank(Player player, String rankId, String lpGroup,
                       String label, String defaultGroup, int days) {
            boolean permanent = (days == 0);

            // dauerhaft: parent set | zeitbasiert: parent addtemp <gruppe> <tage>d
            String cmd = permanent
                ? "lp user " + player.getName() + " parent set " + lpGroup
                : "lp user " + player.getName() + " parent addtemp " + lpGroup + " " + days + "d accumulate";

            // LP-Befehle müssen auf dem Hauptthread laufen
            Bukkit.getScheduler().runTask(plugin, () ->
                Bukkit.dispatchCommand(Bukkit.getConsoleSender(), cmd));

            new BukkitRunnable() {
                @Override public void run() {
                    db.saveRankSession(player.getName(), rankId, lpGroup, defaultGroup, days);
                }
            }.runTaskAsynchronously(plugin);

            player.sendMessage("");
            player.sendMessage(ChatColor.GOLD   + "👑 ════════════════════════════");
            player.sendMessage(ChatColor.YELLOW + "  Rang erhalten: "
                + ChatColor.WHITE + label);
            player.sendMessage(ChatColor.GRAY + "  Dauer: "
                + (permanent ? ChatColor.GREEN + "Dauerhaft"
                             : ChatColor.WHITE + "" + days + " Tag(e)"));
            player.sendMessage(ChatColor.GOLD + "  ════════════════════════════");
            player.sendMessage("");
            player.playSound(player.getLocation(),
                Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 0.8F);
        }

        void checkExpiredRanksForPlayer(Player player) {
            List<RankEntry> expired = db.getExpiredRanks(player.getName());
            for (RankEntry r : expired) {
                db.deleteRankSession(r.id);
                Bukkit.getScheduler().runTask(plugin, () -> {
                    // Rang-Gruppe entfernen statt alles überschreiben
                    Bukkit.dispatchCommand(Bukkit.getConsoleSender(),
                        "lp user " + player.getName() + " parent remove " + r.group);
                    player.sendMessage(ChatColor.RED + "👑 Dein Rang "
                        + ChatColor.WHITE + r.rankId
                        + ChatColor.RED  + " ist abgelaufen.");
                });
            }
        }

        void startExpiryChecker() {
            new BukkitRunnable() {
                @Override public void run() {
                    for (Player p : Bukkit.getOnlinePlayers()) {
                        new BukkitRunnable() {
                            @Override public void run() {
                                checkExpiredRanksForPlayer(p);
                            }
                        }.runTaskAsynchronously(plugin);
                    }
                }
            }.runTaskTimer(plugin, 20L * 300, 20L * 300);
        }

        List<RankEntry> getActiveRanks(String playerName) {
            return db.getActiveRanks(playerName);
        }

        static class RankEntry {
            final int     id;
            final String  rankId, group, defaultGroup, expiresAt;
            final boolean permanent;
            RankEntry(int id, String rankId, String group, String defaultGroup,
                      String expiresAt, boolean permanent) {
                this.id           = id;
                this.rankId       = rankId;
                this.group        = group;
                this.defaultGroup = defaultGroup;
                this.expiresAt    = expiresAt;
                this.permanent    = permanent;
            }
        }
    }


    // ===========================================================
    // RELOAD COMMAND
    // ===========================================================

    private class ReloadCommand implements CommandExecutor {
        @Override
        public boolean onCommand(CommandSender sender, Command command,
                                  String label, String[] args) {
            // /wpis reload
            if (args.length == 0 || !args[0].equalsIgnoreCase("reload")) {
                sender.sendMessage(ChatColor.YELLOW + "» IngameShop Pro");
                sender.sendMessage(ChatColor.GRAY   + "  /wpis reload " + ChatColor.WHITE + "– Config & Sell-Items neu laden");
                return true;
            }
            if (!sender.hasPermission("ingameshop.reload")) {
                sender.sendMessage(ChatColor.RED + "✗ Keine Berechtigung.");
                return true;
            }

            sender.sendMessage(ChatColor.YELLOW + "⟳ IngameShop wird neu geladen...");

            // Config neu laden
            reloadConfig();

            String domain = getConfig().getString("wordpress-url", "http://localhost");
            if (domain.endsWith("/")) domain = domain.substring(0, domain.length() - 1);
            wpBase = domain + "/wp-json/wis/v1";

            wpUrlPending        = wpBase + "/pending_orders";
            wpUrlPendingOffline = wpBase + "/pending_offline";
            wpUrlExecute        = wpBase + "/execute_order";
            wpUrlComplete       = wpBase + "/complete_order";
            wpUrlCancel         = wpBase + "/cancel_order";
            wpUrlSellItems      = wpBase + "/sell_items";
            wpUrlSell           = wpBase + "/sell_item";

            targetServer      = getConfig().getString("server-name",       "survival").toLowerCase();
            currency          = getConfig().getString("currency-name",     "Coins");
            apiKey            = getConfig().getString("api-key",           "");
            debug             = getConfig().getBoolean("debug-mode",       false);
            flyRedeemDisabled = getConfig().getBoolean("fly-redeem-disabled", false);
            incomeReceiver    = getConfig().getString("income-receiver",   "");
            sellEnabled       = getConfig().getBoolean("sell.enabled",     true);
            sellPriceOffset   = getConfig().getDouble("sell.price-offset", 0.0);

            // Sell-Cache leeren und Items neu laden
            sellCache.clear();
            sellAmountPage.clear();
            startSellItemPolling();

            sender.sendMessage(ChatColor.GREEN + "✔ Config neu geladen!"
                + ChatColor.GRAY + " (Server: " + targetServer
                + " | Sell: " + (sellEnabled ? "aktiv" : "deaktiviert")
                + " | Offset: " + sellPriceOffset + " %)");

            getLogger().info("[IngameShop] Config-Reload durch: " + sender.getName());
            return true;
        }
    }

    // ===========================================================
    // SELL COMMAND
    // ===========================================================

    /** /sell [hand|all] */
    private class SellCommand implements CommandExecutor {
        @Override
        public boolean onCommand(CommandSender sender, Command command,
                                  String label, String[] args) {
            if (!(sender instanceof Player)) { sender.sendMessage("Nur für Spieler."); return true; }
            if (!sellEnabled) {
                sender.sendMessage(ChatColor.RED + "✗ Der Ankauf ist auf diesem Server deaktiviert.");
                return true;
            }
            Player p = (Player) sender;

            // /sell hand  → sofort Item in der Hand verkaufen (1 Stack)
            if (args.length > 0 && args[0].equalsIgnoreCase("hand")) {
                sellHandItem(p);
                return true;
            }
            // /sell all   → alle verkaufbaren Items aus dem Inventar verkaufen
            if (args.length > 0 && args[0].equalsIgnoreCase("all")) {
                sellAllItems(p);
                return true;
            }
            // /sell       → GUI öffnen
            openSellGUI(p, 0);
            return true;
        }
    }

    // ===========================================================
    // SELL GUI
    // ===========================================================

    private void openSellGUI(Player player, int page) {
        new BukkitRunnable() {
            @Override public void run() {
                List<SellManager.SellEntry> entries = SellManager.getInstance().getEntries();
                Bukkit.getScheduler().runTask(IngameShopSpigot.this,
                    () -> renderSellGUI(player, entries, page));
            }
        }.runTaskAsynchronously(this);
    }

    private void renderSellGUI(Player player, List<SellManager.SellEntry> entries, int page) {
        final int PAGE_SIZE = 45;
        int totalPages = Math.max(1, (int) Math.ceil(entries.size() / (double) PAGE_SIZE));
        page = Math.max(0, Math.min(page, totalPages - 1));

        sellAmountPage.put(player.getUniqueId(), page);
        sellCache.put(player.getUniqueId(), entries);

        Inventory gui = Bukkit.createInventory(null, 54, GUI_SELL_TITLE);

        // Pane-Reihe unten
        ItemStack pane = new ItemStack(Material.BLACK_STAINED_GLASS_PANE);
        ItemMeta pM = pane.getItemMeta(); pM.setDisplayName(" "); pane.setItemMeta(pM);
        for (int i = 45; i < 54; i++) gui.setItem(i, pane);

        int start = page * PAGE_SIZE;
        int end   = Math.min(start + PAGE_SIZE, entries.size());

        for (int i = start; i < end; i++) {
            SellManager.SellEntry e = entries.get(i);
            ItemStack display = parseItem(e.itemId);
            if (display == null) display = new ItemStack(Material.BARRIER);

            // Wie viel hat der Spieler davon?
            int playerHas = countItemInInventory(player, e.itemId);
            double totalValue = playerHas * applyOffset(e.sellPrice);

            ItemMeta meta = display.getItemMeta();
            meta.setDisplayName(ChatColor.YELLOW + "" + ChatColor.BOLD + e.name);
            List<String> lore = new ArrayList<>();
            lore.add(ChatColor.GRAY  + "──────────────────────");
            lore.add(ChatColor.WHITE + "Ankauf:  " + ChatColor.GREEN
                + String.format("%.2f", applyOffset(e.sellPrice)) + " " + currency + " /Stück");
            lore.add(ChatColor.WHITE + "Kaufpreis: " + ChatColor.GRAY
                + e.buyPrice + " " + currency);
            lore.add(ChatColor.GRAY  + "──────────────────────");
            if (playerHas > 0) {
                lore.add(ChatColor.AQUA  + "Du hast: " + ChatColor.WHITE + playerHas + "x");
                lore.add(ChatColor.GREEN + "Wert:    " + ChatColor.WHITE
                    + String.format("%.2f", totalValue) + " " + currency);
                lore.add(ChatColor.GRAY  + "──────────────────────");
                lore.add(ChatColor.YELLOW + "▶ Linksklick:  " + ChatColor.WHITE + "Alles verkaufen");
                lore.add(ChatColor.YELLOW + "▶ Rechtsklick: " + ChatColor.WHITE + "1 Stack verkaufen");
            } else {
                lore.add(ChatColor.RED + "✗ Nicht in deinem Inventar");
            }
            meta.setLore(lore);
            display.setItemMeta(meta);
            gui.setItem(i - start, display);
        }

        // Nav
        if (page > 0) {
            ItemStack prev = new ItemStack(Material.ARROW);
            ItemMeta pm = prev.getItemMeta();
            pm.setDisplayName(ChatColor.YELLOW + "◀ Vorherige Seite");
            prev.setItemMeta(pm);
            gui.setItem(45, prev);
        }
        if (page < totalPages - 1) {
            ItemStack next = new ItemStack(Material.ARROW);
            ItemMeta nm = next.getItemMeta();
            nm.setDisplayName(ChatColor.YELLOW + "Nächste Seite ▶");
            next.setItemMeta(nm);
            gui.setItem(53, next);
        }

        // Info
        ItemStack info = new ItemStack(Material.GOLD_INGOT);
        ItemMeta im = info.getItemMeta();
        im.setDisplayName(ChatColor.GOLD + "" + ChatColor.BOLD + "Item-Ankauf");
        im.setLore(Arrays.asList(
            ChatColor.GRAY + "Seite " + (page + 1) + " / " + totalPages,
            ChatColor.GRAY + "" + entries.size() + " ankaufbare Items",
            ChatColor.GRAY + "──────────────────────",
            ChatColor.WHITE + "/sell all " + ChatColor.GRAY + "→ alles auf einmal verkaufen",
            ChatColor.WHITE + "/sell hand " + ChatColor.GRAY + "→ Item in der Hand verkaufen"
        ));
        info.setItemMeta(im);
        gui.setItem(49, info);

        if (entries.isEmpty()) {
            ItemStack empty = new ItemStack(Material.BARRIER);
            ItemMeta em = empty.getItemMeta();
            em.setDisplayName(ChatColor.RED + "Keine ankaufbaren Items konfiguriert");
            empty.setItemMeta(em);
            gui.setItem(22, empty);
        }

        player.openInventory(gui);
    }

    // ===========================================================
    // SELL INVENTORY CLICK
    // ===========================================================

    // Wird in onInventoryClick() aufgerufen – dort einhaken
    private boolean handleSellClick(InventoryClickEvent event) {
        String title = event.getView().getTitle();
        if (!GUI_SELL_TITLE.equals(title)) return false;

        event.setCancelled(true);
        if (!(event.getWhoClicked() instanceof Player)) return true;
        Player p   = (Player) event.getWhoClicked();
        int    slot = event.getRawSlot();

        List<SellManager.SellEntry> entries = sellCache.get(p.getUniqueId());
        if (entries == null) return true;

        int page  = sellAmountPage.getOrDefault(p.getUniqueId(), 0);
        int total = (int) Math.ceil(entries.size() / 45.0);

        // Navigation
        if (slot == 45 && page > 0) { renderSellGUI(p, entries, page - 1); return true; }
        if (slot == 53 && page < total - 1) { renderSellGUI(p, entries, page + 1); return true; }
        if (slot < 0 || slot > 44) return true;

        int idx = page * 45 + slot;
        if (idx >= entries.size()) return true;

        SellManager.SellEntry entry = entries.get(idx);
        boolean rightClick = event.getClick().isRightClick();

        // Stack (64) oder alles verkaufen
        int sellAmount = rightClick
            ? Math.min(64, countItemInInventory(p, entry.itemId))
            : countItemInInventory(p, entry.itemId);

        if (sellAmount <= 0) {
            p.sendMessage(ChatColor.RED + "✗ Du hast kein " + entry.name + " im Inventar.");
            return true;
        }

        processSell(p, entry, sellAmount, () -> {
            // GUI neu rendern nach Verkauf
            List<SellManager.SellEntry> fresh = sellCache.get(p.getUniqueId());
            if (fresh != null) renderSellGUI(p, fresh, sellAmountPage.getOrDefault(p.getUniqueId(), 0));
        });
        return true;
    }

    // ===========================================================
    // SELL LOGIC
    // ===========================================================

    private void sellHandItem(Player player) {
        ItemStack hand = player.getInventory().getItemInMainHand();
        if (hand == null || hand.getType() == Material.AIR) {
            player.sendMessage(ChatColor.RED + "✗ Halte ein Item in der Hand.");
            return;
        }
        String materialName = hand.getType().name().toLowerCase();
        SellManager.SellEntry entry = SellManager.getInstance().findByMaterial(materialName);
        if (entry == null) {
            player.sendMessage(ChatColor.RED + "✗ Dieses Item wird nicht angekauft.");
            return;
        }
        int amount = hand.getAmount();
        processSell(player, entry, amount, null);
    }

    private void sellAllItems(Player player) {
        List<SellManager.SellEntry> entries = SellManager.getInstance().getEntries();
        if (entries.isEmpty()) {
            player.sendMessage(ChatColor.RED + "✗ Keine ankaufbaren Items konfiguriert.");
            return;
        }
        double totalEarned = 0;
        int    totalItems  = 0;
        for (SellManager.SellEntry entry : entries) {
            int count = countItemInInventory(player, entry.itemId);
            if (count <= 0) continue;
            double price   = applyOffset(entry.sellPrice);
            double earned  = price * count;
            removeItemsFromInventory(player, entry.itemId, count);
            totalEarned += earned;
            totalItems  += count;
        }
        if (totalItems == 0) {
            player.sendMessage(ChatColor.YELLOW + "✗ Keine ankaufbaren Items im Inventar.");
            return;
        }
        final double finalEarned = totalEarned;
        final int    finalItems  = totalItems;
        new BukkitRunnable() {
            @Override public void run() {
                // Batch: kein individueller WP-Call, direkt Vault
                Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
                    econ.depositPlayer(player, finalEarned);
                    player.sendMessage(ChatColor.GREEN + "💰 /sell all: "
                        + ChatColor.WHITE + finalItems + " Items"
                        + ChatColor.GREEN + " verkauft für "
                        + ChatColor.YELLOW + String.format("%.2f", finalEarned)
                        + " " + currency + ChatColor.GREEN + "!");
                    player.playSound(player.getLocation(),
                        Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 0.8F, 1.0F);
                });
            }
        }.runTaskAsynchronously(this);
    }

    private void processSell(Player player, SellManager.SellEntry entry,
                              int amount, Runnable afterCallback) {
        double pricePerItem = applyOffset(entry.sellPrice);
        double total        = pricePerItem * amount;

        // Items aus dem Inventar nehmen (sync)
        int removed = removeItemsFromInventory(player, entry.itemId, amount);
        if (removed == 0) {
            player.sendMessage(ChatColor.RED + "✗ Keine " + entry.name + " im Inventar.");
            return;
        }
        double actualTotal = pricePerItem * removed;

        new BukkitRunnable() {
            @Override public void run() {
                try {
                    HttpURLConnection conn = openAuthConnection(wpUrlSell, "POST");
                    String body = "{\"player\":\"" + player.getName()
                        + "\",\"server\":\"" + targetServer
                        + "\",\"item_id\":\"" + entry.itemId
                        + "\",\"quantity\":" + removed + "}";
                    writeJson(conn, body);
                    int code = conn.getResponseCode();
                    if (code == 200) {
                        String resp = readResponse(conn);
                        JsonObject json = JsonParser.parseString(resp).getAsJsonObject();
                        double wpTotal = json.has("total") ? json.get("total").getAsDouble() : actualTotal;
                        // Offset nochmals anwenden falls config abweicht
                        double finalPay = wpTotal + (wpTotal * sellPriceOffset / 100.0);
                        finalPay = Math.max(0, finalPay);
                        final double pay = finalPay;
                        Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
                            econ.depositPlayer(player, pay);
                            player.sendMessage(ChatColor.GREEN + "💰 "
                                + ChatColor.WHITE + removed + "x " + entry.name
                                + ChatColor.GREEN + " verkauft → "
                                + ChatColor.YELLOW + String.format("%.2f", pay)
                                + " " + currency + ChatColor.GREEN + "!");
                            player.playSound(player.getLocation(),
                                Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 0.8F, 1.1F);
                            if (afterCallback != null) afterCallback.run();
                        });
                    } else {
                        // WP nicht erreichbar → Fallback: direkt auszahlen
                        final double fallback = actualTotal;
                        Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
                            econ.depositPlayer(player, fallback);
                            player.sendMessage(ChatColor.GREEN + "💰 "
                                + ChatColor.WHITE + removed + "x " + entry.name
                                + ChatColor.GREEN + " verkauft → "
                                + ChatColor.YELLOW + String.format("%.2f", fallback)
                                + " " + currency + ChatColor.GREEN + "!");
                            if (debug) getLogger().warning("WP sell_item HTTP " + code + " – Fallback genutzt");
                            if (afterCallback != null) afterCallback.run();
                        });
                    }
                } catch (Exception ex) {
                    final double fallback = actualTotal;
                    Bukkit.getScheduler().runTask(IngameShopSpigot.this, () -> {
                        econ.depositPlayer(player, fallback);
                        player.sendMessage(ChatColor.GREEN + "💰 "
                            + removed + "x " + entry.name + " → "
                            + String.format("%.2f", fallback) + " " + currency);
                        if (debug) getLogger().log(Level.WARNING, "sell_item Fehler", ex);
                        if (afterCallback != null) afterCallback.run();
                    });
                }
            }
        }.runTaskAsynchronously(this);
    }

    /** Wendet den config sell.price-offset an (z.B. -10.0 = -10 %) */
    private double applyOffset(double basePrice) {
        if (sellPriceOffset == 0.0) return basePrice;
        return Math.max(0, basePrice + basePrice * sellPriceOffset / 100.0);
    }

    /** Zählt wie viele Items des Typs im Inventar sind */
    private int countItemInInventory(Player player, String itemId) {
        ItemStack template = parseItem(itemId);
        if (template == null) return 0;
        Material mat = template.getType();
        int count = 0;
        for (ItemStack stack : player.getInventory().getContents()) {
            if (stack != null && stack.getType() == mat) count += stack.getAmount();
        }
        return count;
    }

    /** Entfernt bis zu `amount` Items aus dem Inventar, gibt tatsächlich entfernten Betrag zurück */
    private int removeItemsFromInventory(Player player, String itemId, int amount) {
        ItemStack template = parseItem(itemId);
        if (template == null) return 0;
        Material mat     = template.getType();
        int      removed = 0;
        ItemStack[] contents = player.getInventory().getContents();
        for (int i = 0; i < contents.length; i++) {
            if (removed >= amount) break;
            ItemStack stack = contents[i];
            if (stack == null || stack.getType() != mat) continue;
            int take = Math.min(stack.getAmount(), amount - removed);
            removed += take;
            if (take == stack.getAmount()) contents[i] = null;
            else stack.setAmount(stack.getAmount() - take);
        }
        player.getInventory().setContents(contents);
        return removed;
    }

    // ===========================================================
    // FLY ABO MANAGER
    // ===========================================================

    private class FlyAboManager {

        private final IngameShopSpigot plugin;
        private final FlyCodeManager   db;

        FlyAboManager(IngameShopSpigot plugin, FlyCodeManager db) {
            this.plugin = plugin;
            this.db     = db;
            db.createAboTable();
        }

        // ── Abo aktivieren (über Order) ────────────────────────────────

        void grantAbo(Player player, String label, int monthlyPrice) {
            db.saveAbo(player.getName(), label, monthlyPrice);
            Bukkit.getScheduler().runTask(plugin, () -> {
                player.sendMessage("");
                player.sendMessage(ChatColor.GOLD   + "✈ ════════════════════════════");
                player.sendMessage(ChatColor.YELLOW + "  Fly-Abo aktiviert!");
                player.sendMessage(ChatColor.GRAY   + "  Paket: "       + ChatColor.WHITE + label);
                player.sendMessage(ChatColor.GRAY   + "  Monatspreis: " + ChatColor.WHITE + monthlyPrice + " " + currency);
                player.sendMessage(ChatColor.GRAY   + "  Tägl. Limit: " + ChatColor.WHITE
                    + flyManager.formatTime(flyAboMaxDailySec));
                player.sendMessage(ChatColor.GRAY   + "  Nächste Abbuchung: " + ChatColor.YELLOW + "1. des Folgemonats");
                player.sendMessage(ChatColor.GRAY   + "  Status: "      + ChatColor.WHITE + "/flyabo");
                player.sendMessage(ChatColor.GOLD   + "  ════════════════════════════");
                player.sendMessage("");
                player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 1.2F);
            });
        }

        void grantAboByName(String playerName, String label, int monthlyPrice) {
            db.saveAbo(playerName, label, monthlyPrice);
        }

        void notifyGranted(Player player, String label, int monthlyPrice) {
            Bukkit.getScheduler().runTask(plugin, () -> {
                player.sendMessage("");
                player.sendMessage(ChatColor.GOLD   + "✈ ════════════════════════════");
                player.sendMessage(ChatColor.YELLOW + "  Fly-Abo aktiviert!");
                player.sendMessage(ChatColor.GRAY   + "  Paket: "       + ChatColor.WHITE + label);
                player.sendMessage(ChatColor.GRAY   + "  Monatspreis: " + ChatColor.WHITE + monthlyPrice + " " + currency);
                player.sendMessage(ChatColor.GRAY   + "  Tägl. Limit: " + ChatColor.WHITE
                    + flyManager.formatTime(flyAboMaxDailySec));
                player.sendMessage(ChatColor.GRAY   + "  Status: "      + ChatColor.WHITE + "/flyabo");
                player.sendMessage(ChatColor.GOLD   + "  ════════════════════════════");
                player.sendMessage("");
                player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 1.2F);
            });
        }

        // ── Kündigung durch Spieler ────────────────────────────────────

        boolean cancelAbo(String playerName) {
            return db.cancelAbo(playerName);
        }

        // ── Status lesen ──────────────────────────────────────────────

        AboEntry getActiveAbo(String playerName) {
            return db.getActiveAbo(playerName);
        }

        int getUsedTodaySec(String playerName) {
            return db.getUsedTodaySec(playerName);
        }

        // ── Beim Join: Abo prüfen + ggf. Zahlung-fehlgeschlagen-Meldung ──

        void onPlayerJoin(Player player) {
            AboEntry abo = db.getActiveAbo(player.getName());

            // Abgelaufene Einträge bereinigen
            db.cleanupExpiredAbos(player.getName());
            abo = db.getActiveAbo(player.getName());

            if (abo == null) return;

            // Zahlungsausfall-Hinweis
            final AboEntry finalAbo = abo;
            if ("payment_failed".equals(abo.cancellationReason)) {
                Bukkit.getScheduler().runTask(plugin, () -> {
                    player.sendMessage("");
                    player.sendMessage(ChatColor.RED    + "✈ ════════════════════════════");
                    player.sendMessage(ChatColor.RED    + "  Fly-Abo: Zahlung fehlgeschlagen!");
                    player.sendMessage(ChatColor.GRAY   + "  Dein Abo konnte nicht verlängert werden,");
                    player.sendMessage(ChatColor.GRAY   + "  da dein Kontostand nicht ausreichte.");
                    player.sendMessage(ChatColor.GRAY   + "  Es läuft am " + ChatColor.YELLOW + finalAbo.periodEnd
                        + ChatColor.GRAY + " aus.");
                    player.sendMessage(ChatColor.WHITE  + "  Neues Abo: " + ChatColor.AQUA + "viper-network.de");
                    player.sendMessage(ChatColor.RED    + "  ════════════════════════════");
                    player.sendMessage("");
                    player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1.0F, 0.5F);
                });
            }

            if (abo.cancelled) return; // Gekündigtes Abo: kein Fly mehr gewähren wenn period_end heute

            int usedToday = db.getUsedTodaySec(player.getName());
            int remaining = Math.max(0, flyAboMaxDailySec - usedToday);
            if (remaining <= 0) {
                Bukkit.getScheduler().runTask(plugin, () -> {
                    player.sendMessage(ChatColor.YELLOW + "✈ Fly-Abo aktiv – Tageslimit heute bereits aufgebraucht.");
                    player.sendMessage(ChatColor.GRAY   + "  Morgen stehen dir wieder "
                        + ChatColor.WHITE + flyManager.formatTime(flyAboMaxDailySec)
                        + ChatColor.GRAY  + " zur Verfügung.");
                });
                return;
            }

            final int grantSec = remaining;
            Bukkit.getScheduler().runTask(plugin, () ->
                flyManager.grantFlyWithAboTracking(player, grantSec, finalAbo.label,
                    player.getName(), flyAboMaxDailySec));
        }

        // ── Monatlicher Billing-Check (läuft täglich) ─────────────────

        void startDailyResetChecker() {
            // Alle 5 Minuten: online Spieler mit Abo ohne aktiven Fly neu versorgen
            new BukkitRunnable() {
                @Override public void run() {
                    for (Player p : Bukkit.getOnlinePlayers()) {
                        new BukkitRunnable() {
                            @Override public void run() {
                                AboEntry abo = db.getActiveAbo(p.getName());
                                if (abo == null || abo.cancelled) return;
                                int usedToday = db.getUsedTodaySec(p.getName());
                                int remaining = Math.max(0, flyAboMaxDailySec - usedToday);
                                if (remaining > 0
                                        && flyManager.getRemainingSeconds(p.getUniqueId()) <= 0) {
                                    Bukkit.getScheduler().runTask(plugin, () ->
                                        flyManager.grantFlyWithAboTracking(p, remaining, abo.label,
                                            p.getName(), flyAboMaxDailySec));
                                }
                            }
                        }.runTaskAsynchronously(plugin);
                    }
                }
            }.runTaskTimer(plugin, 20L * 300, 20L * 300);

            // Täglich um ~00:05 Uhr: Billing-Check für den 1. des Monats
            // Berechne Ticks bis 00:05 Uhr (approximiert: sofort starten, dann täglich wiederholen)
            new BukkitRunnable() {
                @Override public void run() {
                    java.time.LocalDate today = java.time.LocalDate.now();
                    if (today.getDayOfMonth() != 1) return; // Nur am 1. des Monats ausführen

                    new BukkitRunnable() {
                        @Override public void run() {
                            List<FlyAboManager.AboEntry> due = db.getDueBillings();
                            if (due.isEmpty()) return;

                            getLogger().info("[FlyAbo] Billing-Tag: " + due.size() + " Abo(s) zu verlängern.");

                            for (FlyAboManager.AboEntry entry : due) {
                                String playerName = entry.playerName;
                                int price         = entry.monthlyPrice;

                                // Vault-Abbuchung auf Main-Thread
                                Bukkit.getScheduler().runTask(plugin, () -> {
                                    @SuppressWarnings("deprecation")
                                    org.bukkit.OfflinePlayer op = Bukkit.getOfflinePlayer(playerName);
                                    if (op == null || !op.hasPlayedBefore()) {
                                        // Spieler unbekannt → überspringen (nächste Nacht nochmal)
                                        return;
                                    }
                                    boolean hasBalance = (price == 0) || econ.getBalance(op) >= price;
                                    if (hasBalance) {
                                        if (price > 0) {
                                            econ.withdrawPlayer(op, price);
                                            if (!incomeReceiver.isEmpty()) {
                                                @SuppressWarnings("deprecation")
                                                org.bukkit.OfflinePlayer recv =
                                                    Bukkit.getOfflinePlayer(incomeReceiver);
                                                if (recv != null && recv.hasPlayedBefore())
                                                    econ.depositPlayer(recv, price);
                                            }
                                        }
                                        // DB-Verlängerung async
                                        new BukkitRunnable() {
                                            @Override public void run() {
                                                db.renewAbo(playerName);
                                                getLogger().info("[FlyAbo] ✔ Abo verlängert: "
                                                    + playerName + " (" + price + " " + currency + ")");
                                            }
                                        }.runTaskAsynchronously(plugin);

                                        // Online-Benachrichtigung
                                        Player online = Bukkit.getPlayer(playerName);
                                        if (online != null && online.isOnline()) {
                                            online.sendMessage("");
                                            online.sendMessage(ChatColor.GOLD   + "✈ ════════════════════════════");
                                            online.sendMessage(ChatColor.GREEN  + "  Fly-Abo verlängert!");
                                            online.sendMessage(ChatColor.GRAY   + "  Abgebucht: "
                                                + ChatColor.WHITE + price + " " + currency);
                                            online.sendMessage(ChatColor.GRAY   + "  Läuft bis: "
                                                + ChatColor.YELLOW + "Ende des Monats");
                                            online.sendMessage(ChatColor.GOLD   + "  ════════════════════════════");
                                            online.sendMessage("");
                                            online.playSound(online.getLocation(),
                                                Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 1.0F);
                                        }
                                    } else {
                                        // Zahlung fehlgeschlagen → Abo kündigen
                                        new BukkitRunnable() {
                                            @Override public void run() {
                                                db.cancelAboPaymentFailed(playerName);
                                                getLogger().warning("[FlyAbo] ✘ Zahlung fehlgeschlagen: "
                                                    + playerName + " (benötigt: " + price + " " + currency + ")");
                                            }
                                        }.runTaskAsynchronously(plugin);

                                        Player online = Bukkit.getPlayer(playerName);
                                        if (online != null && online.isOnline()) {
                                            online.sendMessage("");
                                            online.sendMessage(ChatColor.RED    + "✈ ════════════════════════════");
                                            online.sendMessage(ChatColor.RED    + "  Fly-Abo: Zahlung fehlgeschlagen!");
                                            online.sendMessage(ChatColor.GRAY   + "  Benötigt: "
                                                + ChatColor.WHITE + price + " " + currency);
                                            online.sendMessage(ChatColor.GRAY   + "  Dein Abo läuft Ende des Monats aus.");
                                            online.sendMessage(ChatColor.WHITE  + "  Verlängern: " + ChatColor.AQUA + "viper-network.de");
                                            online.sendMessage(ChatColor.RED    + "  ════════════════════════════");
                                            online.sendMessage("");
                                            online.playSound(online.getLocation(),
                                                Sound.BLOCK_NOTE_BLOCK_BASS, 1.0F, 0.5F);
                                        }
                                    }
                                });
                            }
                        }
                    }.runTaskAsynchronously(plugin);
                }
            }.runTaskTimer(plugin, 20L * 60 * 5, 20L * 60 * 60 * 24); // Start nach 5 min, dann täglich
        }

        // ── Data class ────────────────────────────────────────────────

        static class AboEntry {
            final String  playerName, label, cancellationReason, nextBillingDate, periodEnd;
            final int     monthlyPrice;
            final boolean cancelled;
            AboEntry(String playerName, String label, int monthlyPrice,
                     boolean cancelled, String cancellationReason,
                     String nextBillingDate, String periodEnd) {
                this.playerName         = playerName;
                this.label              = label;
                this.monthlyPrice       = monthlyPrice;
                this.cancelled          = cancelled;
                this.cancellationReason = cancellationReason;
                this.nextBillingDate    = nextBillingDate;
                this.periodEnd          = periodEnd;
            }
        }
    }

    // ===========================================================
    // PLOT SLOT MANAGER
    // ===========================================================

    private class PlotSlotManager {

        private final IngameShopSpigot plugin;
        private final FlyCodeManager   db;

        PlotSlotManager(IngameShopSpigot plugin, FlyCodeManager db) {
            this.plugin = plugin;
            this.db     = db;
            db.createPlotSlotTables();
        }

        // ── Rang-Basis aus Config ──────────────────────────────────────

        int getRankBase(Player player) {
            int highest = plotRankDefaults.getOrDefault("member", 2);
            for (Map.Entry<String, Integer> entry : plotRankDefaults.entrySet()) {
                if (player.hasPermission("group." + entry.getKey())) {
                    if (entry.getValue() > highest) highest = entry.getValue();
                }
            }
            return highest;
        }

        int getRankBaseByName(String playerName) {
            Player online = Bukkit.getPlayer(playerName);
            if (online != null) return getRankBase(online);
            return plotRankDefaults.getOrDefault("member", 2);
        }

        // ── Gesamtlimit berechnen und LuckPerms setzen ─────────────────

        void applyLimit(String playerName) {
            int extra = db.getExtraSlots(playerName);
            int abo   = db.getPlotAboSlots(playerName);
            int base  = getRankBaseByName(playerName);
            int total = base + extra + abo;
            int last  = db.getLastPerm(playerName);
            Bukkit.getScheduler().runTask(plugin, () -> {
                // Nur die tatsächlich gesetzte alte Permission entfernen
                if (last > 0 && last != total) {
                    Bukkit.dispatchCommand(Bukkit.getConsoleSender(),
                        "lp user " + playerName + " permission unset plots.plot." + last);
                }
                // Neue Permission setzen
                Bukkit.dispatchCommand(Bukkit.getConsoleSender(),
                    "lp user " + playerName + " permission set plots.plot." + total + " true");
            });
            // last_perm async speichern
            new BukkitRunnable() {
                @Override public void run() { db.saveLastPerm(playerName, total); }
            }.runTaskAsynchronously(plugin);
            if (debug) plugin.getLogger().info("[PlotSlots] " + playerName
                + " → Basis=" + base + " Extra=" + extra + " Abo=" + abo
                + " Gesamt=" + total + " (vorher: " + last + ") → plots.plot." + total);
        }

        // ── Einmalige Extra-Slots ─────────────────────────────────────

        void grantExtraSlots(Player player, int slots, String label) {
            db.addExtraSlots(player.getName(), slots);
            applyLimit(player.getName());
            Bukkit.getScheduler().runTask(plugin, () -> {
                int total = getRankBase(player)
                    + db.getExtraSlots(player.getName())
                    + db.getPlotAboSlots(player.getName());
                player.sendMessage("");
                player.sendMessage(ChatColor.GOLD   + "📦 ════════════════════════════");
                player.sendMessage(ChatColor.YELLOW + "  Plot-Slots erweitert!");
                player.sendMessage(ChatColor.GRAY   + "  Paket: "   + ChatColor.WHITE + label);
                player.sendMessage(ChatColor.GRAY   + "  Neu: "     + ChatColor.WHITE + "+" + slots + " permanente Slots");
                player.sendMessage(ChatColor.GRAY   + "  Gesamt: "  + ChatColor.GREEN + total + " Plot-Slots");
                player.sendMessage(ChatColor.GOLD   + "  ════════════════════════════");
                player.sendMessage("");
                player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 1.0F);
            });
        }

        int getExtraSlots(String playerName) { return db.getExtraSlots(playerName); }

        void grantExtraSlotsByName(String playerName, int slots, String label) {
            db.addExtraSlots(playerName, slots);
            applyLimit(playerName);
        }

        // ── Monatliches Abo ───────────────────────────────────────────

        void grantAbo(Player player, int slots, String label, int monthlyPrice) {
            db.savePlotAbo(player.getName(), slots, label, monthlyPrice);
            applyLimit(player.getName());
            Bukkit.getScheduler().runTask(plugin, () -> {
                player.sendMessage("");
                player.sendMessage(ChatColor.GOLD   + "📦 ════════════════════════════");
                player.sendMessage(ChatColor.YELLOW + "  Plot-Abo aktiviert!");
                player.sendMessage(ChatColor.GRAY   + "  Paket: "       + ChatColor.WHITE + label);
                player.sendMessage(ChatColor.GRAY   + "  Slots: "       + ChatColor.WHITE + "+" + slots);
                player.sendMessage(ChatColor.GRAY   + "  Monatspreis: " + ChatColor.WHITE + monthlyPrice + " " + currency);
                player.sendMessage(ChatColor.GRAY   + "  Nächste Abbuchung: " + ChatColor.YELLOW + "1. des Folgemonats");
                player.sendMessage(ChatColor.GRAY   + "  Status: /plotabo");
                player.sendMessage(ChatColor.GOLD   + "  ════════════════════════════");
                player.sendMessage("");
                player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 1.2F);
            });
        }

        void grantAboByName(String playerName, int slots, String label, int monthlyPrice) {
            db.savePlotAbo(playerName, slots, label, monthlyPrice);
            applyLimit(playerName);
        }

        boolean cancelAbo(String playerName) {
            boolean ok = db.cancelPlotAbo(playerName);
            if (ok) applyLimit(playerName);
            return ok;
        }

        int getAboSlots(String playerName) { return db.getPlotAboSlots(playerName); }

        PlotAboEntry getActiveAbo(String playerName) { return db.getActivePlotAbo(playerName); }

        // ── Einfrieren überzähliger Plots ─────────────────────────────

        void freezeExcessPlots(String playerName, int newLimit) {
            Bukkit.getScheduler().runTask(plugin, () -> {
                if (Bukkit.getPluginManager().getPlugin("PlotSquared") == null) return;
                Player online = Bukkit.getPlayer(playerName);
                if (online != null) {
                    online.sendMessage("");
                    online.sendMessage(ChatColor.RED    + "📦 ════════════════════════════");
                    online.sendMessage(ChatColor.RED    + "  Plot-Abo abgelaufen!");
                    online.sendMessage(ChatColor.GRAY   + "  Limit reduziert auf: "
                        + ChatColor.WHITE + newLimit);
                    online.sendMessage(ChatColor.YELLOW + "  Überzählige Plots sind eingefroren.");
                    online.sendMessage(ChatColor.GRAY   + "  Abo erneuern: " + ChatColor.AQUA + "viper-network.de");
                    online.sendMessage(ChatColor.RED    + "  ════════════════════════════");
                    online.sendMessage("");
                    online.playSound(online.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1.0F, 0.5F);
                }
                Bukkit.dispatchCommand(Bukkit.getConsoleSender(),
                    "lp user " + playerName + " meta set frozen_excess_plots true");
            });
        }

        void unfreezeExcessPlots(String playerName) {
            Bukkit.getScheduler().runTask(plugin, () ->
                Bukkit.dispatchCommand(Bukkit.getConsoleSender(),
                    "lp user " + playerName + " meta unset frozen_excess_plots"));
        }

        // ── Monatlicher Billing-Check ─────────────────────────────────

        void startBillingChecker() {
            new BukkitRunnable() {
                @Override public void run() {
                    if (java.time.LocalDate.now().getDayOfMonth() != 1) return;
                    new BukkitRunnable() {
                        @Override public void run() {
                            List<PlotAboEntry> due = db.getDuePlotBillings();
                            if (due.isEmpty()) return;
                            plugin.getLogger().info("[PlotSlots] Billing: " + due.size() + " Abo(s).");
                            for (PlotAboEntry entry : due) {
                                final String pName = entry.playerName;
                                final int    price = entry.monthlyPrice;
                                final int    slots = entry.aboSlots;
                                Bukkit.getScheduler().runTask(plugin, () -> {
                                    @SuppressWarnings("deprecation")
                                    org.bukkit.OfflinePlayer op = Bukkit.getOfflinePlayer(pName);
                                    if (op == null || !op.hasPlayedBefore()) return;
                                    if ((price == 0) || econ.getBalance(op) >= price) {
                                        if (price > 0) {
                                            econ.withdrawPlayer(op, price);
                                            if (!incomeReceiver.isEmpty()) {
                                                @SuppressWarnings("deprecation")
                                                org.bukkit.OfflinePlayer recv = Bukkit.getOfflinePlayer(incomeReceiver);
                                                if (recv != null && recv.hasPlayedBefore())
                                                    econ.depositPlayer(recv, price);
                                            }
                                        }
                                        new BukkitRunnable() {
                                            @Override public void run() {
                                                db.renewPlotAbo(pName);
                                            }
                                        }.runTaskAsynchronously(plugin);
                                        unfreezeExcessPlots(pName);
                                        applyLimit(pName);
                                        Player online = Bukkit.getPlayer(pName);
                                        if (online != null) {
                                            online.sendMessage(ChatColor.GOLD  + "📦 Plot-Abo verlängert! +"
                                                + slots + " Slots | " + price + " " + currency + " abgebucht.");
                                            online.playSound(online.getLocation(),
                                                Sound.ENTITY_PLAYER_LEVELUP, 1.0F, 1.0F);
                                        }
                                    } else {
                                        new BukkitRunnable() {
                                            @Override public void run() {
                                                db.cancelPlotAboPaymentFailed(pName);
                                            }
                                        }.runTaskAsynchronously(plugin);
                                        new BukkitRunnable() {
                                            @Override public void run() {
                                                int newLimit = getRankBaseByName(pName) + db.getExtraSlots(pName);
                                                freezeExcessPlots(pName, newLimit);
                                                // applyLimit kümmert sich um die Permission
                                                applyLimit(pName);
                                            }
                                        }.runTaskAsynchronously(plugin);
                                    }
                                });
                            }
                        }
                    }.runTaskAsynchronously(plugin);
                }
            }.runTaskTimer(plugin, 20L * 60 * 5, 20L * 60 * 60 * 24);
        }

        // ── PlayerJoin ────────────────────────────────────────────────

        void onPlayerJoin(Player player) {
            applyLimit(player.getName());
            if (player.hasPermission("meta.frozen_excess_plots.true")) {
                player.sendMessage(ChatColor.RED  + "📦 Einige deiner Plots sind eingefroren!");
                player.sendMessage(ChatColor.GRAY + "  Grund: Plot-Abo abgelaufen.");
                player.sendMessage(ChatColor.WHITE + "  Erneuern: " + ChatColor.AQUA + "viper-network.de");
            }
        }

        // ── Data class ────────────────────────────────────────────────

        static class PlotAboEntry {
            final String  playerName, label, cancellationReason, nextBillingDate, periodEnd;
            final int     aboSlots, monthlyPrice;
            final boolean cancelled;
            PlotAboEntry(String playerName, String label, int aboSlots, int monthlyPrice,
                         boolean cancelled, String cancellationReason,
                         String nextBillingDate, String periodEnd) {
                this.playerName         = playerName;
                this.label              = label;
                this.aboSlots           = aboSlots;
                this.monthlyPrice       = monthlyPrice;
                this.cancelled          = cancelled;
                this.cancellationReason = cancellationReason;
                this.nextBillingDate    = nextBillingDate;
                this.periodEnd          = periodEnd;
            }
        }
    }

    // ===========================================================
    // SELL MANAGER
    // ===========================================================

    private static class SellManager {

        private static SellManager instance;
        private final List<SellEntry> entries = new ArrayList<>();

        private SellManager() {}

        static SellManager getInstance() {
            if (instance == null) instance = new SellManager();
            return instance;
        }

        synchronized void load(List<SellEntry> fresh) {
            entries.clear();
            entries.addAll(fresh);
        }

        synchronized List<SellEntry> getEntries() {
            return new ArrayList<>(entries);
        }

        synchronized SellEntry findByMaterial(String materialName) {
            String clean = materialName.toLowerCase().replace("minecraft:", "");
            for (SellEntry e : entries) {
                String eid = e.itemId.toLowerCase().replace("minecraft:", "");
                if (eid.equals(clean)) return e;
            }
            return null;
        }

        static class SellEntry {
            final String itemId, name;
            final double buyPrice, sellPrice;

            SellEntry(String itemId, String name, double buyPrice, double sellPrice) {
                this.itemId    = itemId;
                this.name      = name;
                this.buyPrice  = buyPrice;
                this.sellPrice = sellPrice;
            }
        }
    }

    // ===========================================================
    // DATA CLASS
    // ===========================================================

    private static class OrderData {
        final int    id, quantity;
        final String itemId, itemTitle, jsonPayload;
        final double price;

        OrderData(int id, String itemId, String itemTitle,
                  double price, int quantity, String jsonPayload) {
            this.id          = id;
            this.itemId      = itemId;
            this.itemTitle   = itemTitle;
            this.price       = price;
            this.quantity    = quantity;
            this.jsonPayload = jsonPayload;
        }
    }
}