package de.ticketsystem.manager; import de.ticketsystem.TicketPlugin; import de.ticketsystem.database.DatabaseManager; import de.ticketsystem.model.FaqCategory; import de.ticketsystem.model.FaqEntry; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; import java.io.File; import java.io.IOException; import java.util.*; /** * Verwaltet FAQ-Einträge und FAQ-Kategorien. * * Speichermodus wird in config.yml festgelegt: * * faq-storage: file → faqs.yml (Standard, Einzelserver) * faq-storage: mysql → MySQL-Datenbank (BungeeCord / Multi-Server, use-mysql muss true sein) * * Im MySQL-Modus werden Tabellen faq_entries und faq_categories verwendet. * Im Datei-Modus verhält sich der Manager exakt wie bisher (faqs.yml). * * Die öffentliche API (add, edit, delete, getAll …) ist in beiden Modi identisch – * FaqGUI, FaqHandler und ApiHandler müssen nicht angepasst werden. */ public class FaqManager { public static final String UNCATEGORIZED_KEY = "__none__"; private final TicketPlugin plugin; private final boolean useMysql; // ── YAML-Modus ──────────────────────────────────────────────────────── private final File faqFile; private YamlConfiguration faqConfig; // ── In-Memory-State (beide Modi) ────────────────────────────────────── private final List entries = new ArrayList<>(); private final LinkedHashMap categories = new LinkedHashMap<>(); /** Nur im YAML-Modus relevant – im MySQL-Modus liefert AUTO_INCREMENT die ID */ private int nextId = 1; public FaqManager(TicketPlugin plugin) { this.plugin = plugin; this.faqFile = new File(plugin.getDataFolder(), "faqs.yml"); // Speichermodus direkt aus use-mysql lesen – kein separater faq-storage Key nötig this.useMysql = plugin.getConfig().getBoolean("use-mysql", false); if (useMysql) { plugin.getLogger().info("[FaqManager] Speichermodus: MySQL (faq_entries / faq_categories)"); } else { plugin.getLogger().info("[FaqManager] Speichermodus: Datei (faqs.yml)"); } load(); } // ═══════════════════════════════════════════════════════════════════════ // LADEN & SPEICHERN // ═══════════════════════════════════════════════════════════════════════ private void load() { entries.clear(); categories.clear(); nextId = 1; if (useMysql) { loadFromMySQL(); } else { loadFromFile(); } } // ── MySQL-Modus ──────────────────────────────────────────────────────── private void loadFromMySQL() { DatabaseManager db = plugin.getDatabaseManager(); for (FaqCategory cat : db.getFaqCategories()) { categories.put(cat.getKey(), cat); } for (FaqEntry e : db.getFaqEntries()) { e.setCategoryKey(normalizeCategoryKey(e.getCategoryKey())); entries.add(e); if (e.getId() >= nextId) nextId = e.getId() + 1; } if (plugin.isDebug()) { plugin.getLogger().info("[FaqManager] MySQL: " + categories.size() + " Kategorie(n), " + entries.size() + " FAQ(s) geladen."); } } // ── Datei-Modus ──────────────────────────────────────────────────────── private void loadFromFile() { if (!faqFile.exists()) { try { faqFile.getParentFile().mkdirs(); faqFile.createNewFile(); } catch (IOException e) { plugin.getLogger().severe("[FaqManager] Konnte faqs.yml nicht erstellen: " + e.getMessage()); } faqConfig = new YamlConfiguration(); loadDefaults(); saveToFile(); return; } faqConfig = YamlConfiguration.loadConfiguration(faqFile); ConfigurationSection catSection = faqConfig.getConfigurationSection("categories"); if (catSection != null && !catSection.getKeys(false).isEmpty()) { for (String key : catSection.getKeys(false)) { ConfigurationSection cat = catSection.getConfigurationSection(key); if (cat == null) continue; String name = cat.getString("name", capitalize(key)); String color = cat.getString("color", "&7"); String desc = cat.getString("description", ""); categories.put(key.toLowerCase(), new FaqCategory(key, name, color, desc)); } } ConfigurationSection faqSection = faqConfig.getConfigurationSection("faqs"); if (faqSection != null) { for (String key : faqSection.getKeys(false)) { try { int id = Integer.parseInt(key); String question = faqConfig.getString("faqs." + key + ".question", ""); String answer = faqConfig.getString("faqs." + key + ".answer", ""); String category = faqConfig.getString("faqs." + key + ".category", null); if (!question.isBlank() && !answer.isBlank()) { FaqEntry entry = new FaqEntry(id, question, answer); entry.setCategoryKey(normalizeCategoryKey(category)); entries.add(entry); if (id >= nextId) nextId = id + 1; } } catch (NumberFormatException ignored) {} } } entries.sort(Comparator.comparingInt(FaqEntry::getId)); if (plugin.isDebug()) { plugin.getLogger().info("[FaqManager] Datei: " + categories.size() + " Kategorie(n), " + entries.size() + " FAQ(s) geladen."); } } private void loadDefaults() { faqConfig.options().header( "FAQ-System – faqs.yml\n" + "Wird automatisch generiert. Kategorien sind optional.\n" + "Material/Textur der Kategorie-Items: config.yml → gui-settings.faq.category-head-item" ); writeFileCategory("general", "Allgemein", "&e", "Allgemeine Fragen zum Server"); writeFileCategory("rules", "Regeln", "&c", "Fragen zu den Server-Regeln"); writeFileCategory("gameplay", "Gameplay", "&a", "Fragen zum Spielgeschehen"); writeFileCategory("tickets", "Tickets", "&b", "Fragen zum Ticket-System"); categories.put("general", new FaqCategory("general", "Allgemein", "&e", "Allgemeine Fragen zum Server")); categories.put("rules", new FaqCategory("rules", "Regeln", "&c", "Fragen zu den Server-Regeln")); categories.put("gameplay", new FaqCategory("gameplay", "Gameplay", "&a", "Fragen zum Spielgeschehen")); categories.put("tickets", new FaqCategory("tickets", "Tickets", "&b", "Fragen zum Ticket-System")); writeFileEntry(1, "Wie erstelle ich ein Ticket?", "Nutze den Befehl /ticket create [Kategorie] [Prio] .", "tickets"); writeFileEntry(2, "Wie lange dauert die Bearbeitung?", "Unser Support-Team bearbeitet Tickets so schnell wie möglich.", "tickets"); writeFileEntry(3, "Kann ich mein Ticket löschen?", "Ja! Öffne /ticket list und klicke auf dein Ticket.", "tickets"); writeFileEntry(4, "Wie kann ich meinen Support bewerten?", "Mit /ticket rate good/bad nach dem Schließen.", "tickets"); nextId = 5; for (int i = 1; i <= 4; i++) { String q = faqConfig.getString("faqs." + i + ".question", ""); String a = faqConfig.getString("faqs." + i + ".answer", ""); FaqEntry e = new FaqEntry(i, q, a); e.setCategoryKey("tickets"); entries.add(e); } } // ── YAML-Hilfsmethoden ───────────────────────────────────────────────── private void writeFileCategory(String key, String name, String color, String description) { faqConfig.set("categories." + key + ".name", name); faqConfig.set("categories." + key + ".color", color); faqConfig.set("categories." + key + ".description", description); } private void writeFileEntry(int id, String question, String answer, String categoryKey) { faqConfig.set("faqs." + id + ".question", question); faqConfig.set("faqs." + id + ".answer", answer); if (categoryKey != null && !categoryKey.equals(UNCATEGORIZED_KEY)) { faqConfig.set("faqs." + id + ".category", categoryKey); } } private void saveToFile() { if (faqConfig == null) return; try { faqConfig.save(faqFile); } catch (IOException e) { plugin.getLogger().severe("[FaqManager] Konnte faqs.yml nicht speichern: " + e.getMessage()); } } // ═══════════════════════════════════════════════════════════════════════ // PUBLIC API – KATEGORIEN // ═══════════════════════════════════════════════════════════════════════ public boolean hasCategoriesEnabled() { return !categories.isEmpty(); } public List getAllCategories() { return Collections.unmodifiableList(new ArrayList<>(categories.values())); } public FaqCategory getCategoryByKey(String key) { if (key == null) return null; return categories.get(key.toLowerCase()); } public FaqCategory addCategory(String key, String name, String color, String description) { String lowerKey = key.toLowerCase().replaceAll("\\s+", "_"); if (categories.containsKey(lowerKey)) return null; FaqCategory cat = new FaqCategory(lowerKey, name, color, description); categories.put(lowerKey, cat); if (useMysql) { plugin.getDatabaseManager().createFaqCategory(lowerKey, name, color, description); } else { writeFileCategory(lowerKey, name, color, description); saveToFile(); } return cat; } public boolean editCategory(String key, String name, String color, String description) { String lowerKey = key.toLowerCase(); if (!categories.containsKey(lowerKey)) return false; categories.put(lowerKey, new FaqCategory(lowerKey, name, color, description)); if (useMysql) { return plugin.getDatabaseManager().updateFaqCategory(lowerKey, name, color, description); } else { writeFileCategory(lowerKey, name, color, description); saveToFile(); return true; } } public boolean deleteCategory(String key) { String lowerKey = key.toLowerCase(); if (!categories.containsKey(lowerKey)) return false; categories.remove(lowerKey); if (useMysql) { boolean ok = plugin.getDatabaseManager().deleteFaqCategory(lowerKey); // In-Memory synchronisieren entries.stream() .filter(e -> lowerKey.equals(e.getCategoryKey())) .forEach(e -> e.setCategoryKey(UNCATEGORIZED_KEY)); return ok; } else { faqConfig.set("categories." + lowerKey, null); for (FaqEntry entry : entries) { if (lowerKey.equals(entry.getCategoryKey())) { entry.setCategoryKey(UNCATEGORIZED_KEY); faqConfig.set("faqs." + entry.getId() + ".category", null); } } saveToFile(); return true; } } public List getByCategory(String categoryKey) { String normalizedKey = normalizeCategoryKey(categoryKey); List result = new ArrayList<>(); for (FaqEntry e : entries) { if (normalizedKey.equals(e.getCategoryKey())) result.add(e); } return Collections.unmodifiableList(result); } public int countByCategory(String categoryKey) { return getByCategory(categoryKey).size(); } // ═══════════════════════════════════════════════════════════════════════ // PUBLIC API – EINTRÄGE // ═══════════════════════════════════════════════════════════════════════ public List getAll() { return Collections.unmodifiableList(entries); } public FaqEntry getById(int id) { return entries.stream().filter(e -> e.getId() == id).findFirst().orElse(null); } public FaqEntry add(String question, String answer, String categoryKey) { String normalizedKey = normalizeCategoryKey(categoryKey); if (useMysql) { int id = plugin.getDatabaseManager().createFaqEntry(question, answer, normalizedKey); if (id == -1) return null; FaqEntry entry = new FaqEntry(id, question, answer); entry.setCategoryKey(normalizedKey); entries.add(entry); return entry; } else { int id = nextId++; FaqEntry entry = new FaqEntry(id, question, answer); entry.setCategoryKey(normalizedKey); entries.add(entry); writeFileEntry(id, question, answer, normalizedKey); saveToFile(); return entry; } } public FaqEntry add(String question, String answer) { return add(question, answer, null); } public boolean edit(int id, String question, String answer) { FaqEntry entry = getById(id); if (entry == null) return false; entry.setQuestion(question); entry.setAnswer(answer); if (useMysql) { return plugin.getDatabaseManager().updateFaqEntry(id, question, answer); } else { writeFileEntry(id, question, answer, entry.getCategoryKey()); saveToFile(); return true; } } public boolean setCategory(int id, String categoryKey) { FaqEntry entry = getById(id); if (entry == null) return false; String normalizedKey = normalizeCategoryKey(categoryKey); entry.setCategoryKey(normalizedKey); if (useMysql) { return plugin.getDatabaseManager().updateFaqEntryCategory(id, normalizedKey); } else { if (normalizedKey.equals(UNCATEGORIZED_KEY)) { faqConfig.set("faqs." + id + ".category", null); } else { faqConfig.set("faqs." + id + ".category", normalizedKey); } saveToFile(); return true; } } public boolean delete(int id) { FaqEntry entry = getById(id); if (entry == null) return false; entries.remove(entry); if (useMysql) { return plugin.getDatabaseManager().deleteFaqEntry(id); } else { faqConfig.set("faqs." + id, null); saveToFile(); return true; } } public void reload() { load(); } /** Gibt zurück ob MySQL als FAQ-Speicher aktiv ist. */ public boolean isUsingMySQL() { return useMysql; } /** * Migriert FAQ-Daten aus faqs.yml in die MySQL-Datenbank. * * Nur ausführbar wenn use-mysql: true aktiv ist. * Nach erfolgreichem Import wird der In-Memory-Cache neu geladen. * * @return int[2] { importierte Kategorien, importierte Einträge } oder null bei Fehler */ public int[] migrateFaqToMySQL() { if (!useMysql) { plugin.getLogger().warning("[FaqManager] Migration nur im MySQL-Modus möglich (use-mysql: true)."); return null; } if (!faqFile.exists()) { plugin.getLogger().warning("[FaqManager] faqs.yml nicht gefunden – Migration abgebrochen."); return null; } try { YamlConfiguration yml = YamlConfiguration.loadConfiguration(faqFile); DatabaseManager db = plugin.getDatabaseManager(); // Bestehende DB-Einträge als Duplikat-Schutz laden java.util.Set existingKeys = new java.util.HashSet<>(); for (FaqEntry e : db.getFaqEntries()) { existingKeys.add(e.getQuestion().trim().toLowerCase() + "\u0000" + e.getAnswer().trim().toLowerCase()); } // Kategorien importieren int importedCats = 0; ConfigurationSection catSec = yml.getConfigurationSection("categories"); if (catSec != null) { for (String key : catSec.getKeys(false)) { String name = yml.getString("categories." + key + ".name", key); String color = yml.getString("categories." + key + ".color", "&7"); String desc = yml.getString("categories." + key + ".description", ""); if (db.createFaqCategory(key.toLowerCase(), name, color, desc)) importedCats++; } } // Einträge importieren — Duplikat-Prüfung per ID (nicht nur Inhalt) int importedEntries = 0; java.util.Set existingIds = new java.util.HashSet<>(); for (FaqEntry e : db.getFaqEntries()) existingIds.add(e.getId()); ConfigurationSection faqSec = yml.getConfigurationSection("faqs"); if (faqSec != null) { for (String key : faqSec.getKeys(false)) { String question = yml.getString("faqs." + key + ".question", ""); String answer = yml.getString("faqs." + key + ".answer", ""); String category = yml.getString("faqs." + key + ".category", null); if (question.isBlank() || answer.isBlank()) continue; // Duplikat-Check: gleiche Frage UND Antwort (normalisiert) String dupKey = question.trim().toLowerCase() + "\u0000" + answer.trim().toLowerCase(); if (existingKeys.contains(dupKey)) continue; int id = db.createFaqEntry(question, answer, category != null ? category : UNCATEGORIZED_KEY); if (id != -1) { importedEntries++; existingKeys.add(dupKey); } } } plugin.getLogger().info("[FaqManager] Migration: " + importedCats + " Kategorie(n), " + importedEntries + " Eintrag/Einträge importiert."); load(); return new int[]{importedCats, importedEntries}; } catch (Exception e) { plugin.getLogger().severe("[FaqManager] Fehler bei Migration: " + e.getMessage()); return null; } } /** * Exportiert alle FAQ-Daten aus MySQL zurück in die faqs.yml. * Nützlich beim Wechsel von MySQL zurück auf Datei-Modus. * Überschreibt eine bestehende faqs.yml nach Sicherheits-Backup. * * @return int[2] { exportierte Kategorien, exportierte Einträge } oder null bei Fehler */ public int[] migrateFaqToFile() { if (!useMysql) { plugin.getLogger().warning("[FaqManager] Rückmigration nur im MySQL-Modus möglich."); return null; } try { DatabaseManager db = plugin.getDatabaseManager(); // Backup der bestehenden faqs.yml if (faqFile.exists()) { String ts = new java.text.SimpleDateFormat("yyyy-MM-dd_HH-mm-ss") .format(new java.util.Date()); java.io.File backup = new java.io.File(faqFile.getParent(), "faqs_backup_" + ts + ".yml"); java.nio.file.Files.copy(faqFile.toPath(), backup.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING); plugin.getLogger().info("[FaqManager] Backup erstellt: " + backup.getName()); } YamlConfiguration out = new YamlConfiguration(); // Kategorien schreiben int exportedCats = 0; for (FaqCategory cat : db.getFaqCategories()) { out.set("categories." + cat.getKey() + ".name", cat.getName()); out.set("categories." + cat.getKey() + ".color", cat.getColor()); out.set("categories." + cat.getKey() + ".description", cat.getDescription()); exportedCats++; } // Einträge schreiben int exportedEntries = 0; for (FaqEntry e : db.getFaqEntries()) { String base = "faqs." + e.getId(); out.set(base + ".question", e.getQuestion()); out.set(base + ".answer", e.getAnswer()); if (e.hasCategory()) out.set(base + ".category", e.getCategoryKey()); exportedEntries++; } out.save(faqFile); plugin.getLogger().info("[FaqManager] Rückmigration: " + exportedCats + " Kategorie(n), " + exportedEntries + " Eintrag/Einträge in faqs.yml gespeichert."); return new int[]{exportedCats, exportedEntries}; } catch (Exception e) { plugin.getLogger().severe("[FaqManager] Fehler bei Rückmigration: " + e.getMessage()); return null; } } // ═══════════════════════════════════════════════════════════════════════ // HILFSMETHODEN // ═══════════════════════════════════════════════════════════════════════ private String normalizeCategoryKey(String key) { if (key == null || key.isBlank()) return UNCATEGORIZED_KEY; String lower = key.toLowerCase(); if (lower.equals(UNCATEGORIZED_KEY)) return UNCATEGORIZED_KEY; if (!categories.isEmpty() && !categories.containsKey(lower)) return UNCATEGORIZED_KEY; return lower; } private static String capitalize(String s) { if (s == null || s.isEmpty()) return s; return Character.toUpperCase(s.charAt(0)) + s.substring(1).toLowerCase(); } }