import java.util.*;
import java.io.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.nio.charset.StandardCharsets;

public class KnowledgeBase {

    private static final String KNOWLEDGE_FILE = "knowledge.txt";
    private static final String DOC_SEPARATOR = "---END-DOC---\n"; 
    private static final Pattern TITLE_PATTERN = Pattern.compile("^TITEL:\\s*(.*)", Pattern.MULTILINE);
    private static final Pattern TAGS_PATTERN = Pattern.compile("^TAGS:\\s*(.*)", Pattern.MULTILINE);
    private static final Pattern SCORE_PATTERN = Pattern.compile("^SCORE:\\s*(\\d+)", Pattern.MULTILINE); 
    
    private List<String> documents;
    private Map<String, Double> idfScores;
    private Map<String, Map<String, Double>> tfidfVectors;
    private Set<String> vocabulary;
    private static final double MIN_SCORE_THRESHOLD = 0.1; // Bleibt bei 0.1
    
    private static class DocumentScore {
        String title;
        String content;
        double score;

        public DocumentScore(String title, String content, double score) {
            this.title = title;
            this.content = content;
            this.score = score;
        }
    }

    public KnowledgeBase() {
        createEmptyKnowledgeFile(false);
    }
    
    public void createEmptyKnowledgeFile(boolean force) {
         if (!Files.exists(Paths.get(KNOWLEDGE_FILE)) || force) {
            try {
                // Beispiel-Dokumente
                String initialContent = "TITEL: IP-Adresse statisch konfigurieren (Ubuntu/Debian, netplan)\n" +
                                        "TAGS: ubuntu, debian, netzwerk, ip, netplan\n" +
                                        "SCORE: 0\n" +
                                        "Die Netzwerkkonfiguration unter aktuellen Ubuntu-Systemen erfolgt über Netplan.\n" +
                                        "Die Konfigurationsdatei liegt typischerweise unter `/etc/netplan/01-netcfg.yaml`.\n" +
                                        "Ersetzen Sie `eth0`, die IP-Adresse und das Gateway entsprechend.\n" +
                                        "network:\n  version: 2\n  renderer: networkd\n  ethernets:\n    eth0:\n      dhcp4: no\n      addresses: [192.168.1.10/24]\n      gateway4: 192.168.1.1\n      nameservers:\n          addresses: [8.8.8.8, 8.8.4.4]\n" +
                                        "\nAktivieren Sie die Konfiguration mit:\nsudo netplan apply\n" +
                                        DOC_SEPARATOR +
                                        "TITEL: IP-Adresse statisch konfigurieren (RHEL/CentOS, nmcli)\n" +
                                        "TAGS: rhel, centos, netzwerk, ip, nmcli\n" +
                                        "SCORE: 0\n" +
                                        "Verwenden Sie den NetworkManager-Befehl `nmcli` zur Konfiguration statischer IPs auf RHEL-basierten Systemen.\n" +
                                        "# 1. Verbindung überprüfen/anzeigen\nnmcli con show\n\n# 2. Verbindung ändern (Interface-Namen ersetzen, z.B. 'System eth0')\n" +
                                        "nmcli con mod \"System eth0\" ipv4.method manual ipv4.addresses 192.168.1.20/24 ipv4.gateway 192.168.1.1 ipv4.dns 8.8.8.8,8.8.4.4\n\n# 3. Verbindung aktivieren\nnmcli con up \"System eth0\"\n" +
                                        DOC_SEPARATOR +
                                        "TITEL: Was ist ein Container?\n" +
                                        "TAGS: container, definition, lxc, docker\n" +
                                        "SCORE: 0\n" +
                                        "Ein Container ist eine standardisierte, eigenständige Software-Einheit, die Code und alle seine Abhängigkeiten bündelt. Im Wesentlichen ist es eine leichtgewichtige, portable Umgebung, die zur Isolation von Anwendungen dient und den Host-Kernel nutzt.\n\n# Schlüsselkonzepte:\n* **Namespaces:** Isolieren Systemressourcen (Prozess-IDs, Netzwerk, Dateisystem).\n* **Cgroups (Control Groups):** Beschränken und kontrollieren die Zuweisung von Host-Ressourcen (CPU, Speicher, I/O).\n* **Image:** Eine statische, schreibgeschützte Vorlage, die die Anwendung und ihre Abhängigkeiten enthält.\n* **Laufzeit:** Die aktive Instanz eines Images (der Container selbst).\n" +
                                        DOC_SEPARATOR;
                
                Files.write(Paths.get(KNOWLEDGE_FILE), initialContent.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
                
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        loadDocuments();
    }

    private void loadDocuments() {
        documents = new ArrayList<>();
        try {
            String content = new String(Files.readAllBytes(Paths.get(KNOWLEDGE_FILE)), StandardCharsets.UTF_8);
            documents = Arrays.stream(content.split(DOC_SEPARATOR))
                              .filter(s -> !s.trim().isEmpty())
                              .collect(Collectors.toList());
        } catch (IOException e) {
            System.err.println("Fehler beim Laden der Wissensdatei: " + e.getMessage());
            documents = new ArrayList<>();
        }
        buildIndex();
    }

    private void buildIndex() {
        vocabulary = new HashSet<>();
        Map<String, Integer> docFreq = new HashMap<>(); 
        
        for (String doc : documents) {
            Set<String> uniqueTerms = new HashSet<>();
            for (String term : tokenize(doc)) {
                vocabulary.add(term);
                uniqueTerms.add(term);
            }
            for (String term : uniqueTerms) {
                docFreq.put(term, docFreq.getOrDefault(term, 0) + 1);
            }
        }
        
        idfScores = calculateIdfScores(docFreq, documents.size());
        tfidfVectors = calculateTfidfVectors();
    }

    private List<String> tokenize(String text) {
        // UMGESCHRIEBEN: Nutzung von Character Trigrammen (N=3) zur Tolerierung von Tippfehlern.
        
        // 1. Bereinigung: Kleinbuchstaben und Nicht-Buchstaben/Zahlen durch Leerzeichen ersetzen
        String cleanedText = text.toLowerCase().replaceAll("[^a-zäöüß0-9\\s]", " ");
        
        // 2. Zerlege in Wort-Token
        List<String> wordTokens = Arrays.stream(cleanedText.split("\\s+"))
                                        .filter(s -> !s.isEmpty())
                                        .collect(Collectors.toList());
                                        
        List<String> nGrams = new ArrayList<>();
        final int N = 3; // Trigramm-Größe (empfohlen für Typo-Toleranz)
        
        for (String word : wordTokens) {
            // Generiere N-Gramme für Wörter der Länge >= N
            if (word.length() >= N) {
                for (int i = 0; i <= word.length() - N; i++) {
                    nGrams.add(word.substring(i, i + N));
                }
            } else if (word.length() > 1) { // Kurze Wörter (Länge 2) werden als Ganzes behalten (z.B. "ab", "es")
                nGrams.add(word);
            }
        }
        
        return nGrams;
    }

    private Map<String, Double> calculateTf(String doc) {
        Map<String, Double> tf = new HashMap<>();
        List<String> tokens = tokenize(doc);
        
        for (String term : tokens) {
            tf.put(term, tf.getOrDefault(term, 0.0) + 1.0);
        }
        
        double maxTf = tf.values().stream().mapToDouble(d -> d).max().orElse(1.0);
        tf.replaceAll((term, count) -> count / maxTf);
        
        return tf;
    }

    private Map<String, Double> calculateIdfScores(Map<String, Integer> docFreq, int numDocs) {
        Map<String, Double> idf = new HashMap<>();
        for (String term : vocabulary) {
            int df = docFreq.getOrDefault(term, 0);
            idf.put(term, Math.log( (double) numDocs / (1.0 + df) )); 
        }
        return idf;
    }
    
    private Map<String, Map<String, Double>> calculateTfidfVectors() {
        Map<String, Map<String, Double>> vectors = new HashMap<>();
        for (int i = 0; i < documents.size(); i++) {
            String docId = "DOC_" + i;
            String doc = documents.get(i);
            Map<String, Double> tf = calculateTf(doc);
            Map<String, Double> tfidf = new HashMap<>();
            
            for (String term : tf.keySet()) {
                double tfVal = tf.get(term);
                double idfVal = idfScores.getOrDefault(term, 0.0);
                tfidf.put(term, tfVal * idfVal);
            }
            vectors.put(docId, tfidf);
        }
        return vectors;
    }
    
    private double calculateCosineSimilarity(Map<String, Double> vec1, Map<String, Double> vec2) {
        double dotProduct = 0;
        double norm1 = 0;
        double norm2 = 0;

        for (double val : vec1.values()) {
            norm1 += val * val;
        }

        for (Map.Entry<String, Double> entry : vec2.entrySet()) {
            double val2 = entry.getValue();
            norm2 += val2 * val2;

            double val1 = vec1.getOrDefault(entry.getKey(), 0.0);
            dotProduct += val1 * val2;
        }

        double denominator = Math.sqrt(norm1) * Math.sqrt(norm2);
        return denominator != 0 ? dotProduct / denominator : 0.0;
    }

    public List<String> findBestAnswer(String question) {
        Map<String, Double> questionTf = calculateTf(question);
        Map<String, Double> questionTfIdf = new HashMap<>();
        for (String term : questionTf.keySet()) {
            double tfVal = questionTf.get(term);
            double idfVal = idfScores.getOrDefault(term, 0.0);
            questionTfIdf.put(term, tfVal * idfVal);
        }
        
        List<DocumentScore> scores = new ArrayList<>();

        for (int i = 0; i < documents.size(); i++) {
            String docId = "DOC_" + i;
            Map<String, Double> docTfIdf = tfidfVectors.get(docId);

            if (docTfIdf != null) {
                double score = calculateCosineSimilarity(questionTfIdf, docTfIdf);
                
                if (score > MIN_SCORE_THRESHOLD) { 
                    Matcher m = TITLE_PATTERN.matcher(documents.get(i));
                    String title = m.find() ? m.group(1).trim() : "UNBEKANNT (Index: " + i + ")";
                    
                    scores.add(new DocumentScore(title, documents.get(i), score));
                }
            }
        }
        
        scores.sort(Comparator.comparingDouble((DocumentScore ds) -> ds.score).reversed());

        if (scores.isEmpty()) {
            return Arrays.asList("FEHLER: Ich konnte keinen relevanten Eintrag finden (Ähnlichkeit unter 10%).");
        }
        
        // Die Top 3 Ergebnisse als formatierte Strings zurückgeben
        return scores.stream()
                     .limit(3) 
                     .map(ds -> {
                        // 1. Manuelles SCORE-Feld aus dem rohen Dokument extrahieren
                        Matcher scoreMatcher = SCORE_PATTERN.matcher(ds.content);
                        String scoreValue = scoreMatcher.find() ? scoreMatcher.group(1).trim() : "0";
                         
                        // 2. Inhalt bereinigen (entfernt TAGS und SCORE-Linie)
                        String contentWithoutMetadata = ds.content.replaceAll(TAGS_PATTERN.pattern(), "")
                                                                .replaceAll(SCORE_PATTERN.pattern(), "")
                                                                .trim();
                                                                
                        // 3. NEUE FORMATIERUNG: TITEL zuerst, um die Extraktion in ChatbotGUI zu fixen
                        return String.format("TITEL: %s\n[Übereinstimmung: %.2f%%]\nSCORE: %s\n%s\n---ENDE-ANTWORT---", 
                                             ds.title, ds.score * 100, scoreValue, contentWithoutMetadata);
                     })
                     .collect(Collectors.toList());
    }
    
    // MODIFIZIERT: Methode nimmt nun auch den Suchbegriff entgegen
    public String adjustDocumentScore(String title, String searchQuery) {
        if (title == null || title.isEmpty()) {
            return "FEHLER: Ungültiger Dokumenttitel für Score-Anpassung.";
        }
        
        if (searchQuery == null || searchQuery.trim().isEmpty()) {
             return "FEHLER: Suchbegriff fehlt für das Tagging.";
        }
        
        // Den Suchbegriff als String-Tag vorbereiten, doppelte Anführungszeichen werden im Tag gespeichert, 
        // um den ganzen Satz als ein Tag zu behandeln.
        String newTag = "\"" + searchQuery.trim().replace("\"", "'") + "\"";
        
        try {
            List<String> lines = Files.readAllLines(Paths.get(KNOWLEDGE_FILE), StandardCharsets.UTF_8);
            List<String> newLines = new ArrayList<>();
            boolean documentFound = false;
            boolean scoreUpdated = false;
            boolean tagsUpdated = false;
            
            for (int i = 0; i < lines.size(); i++) {
                String line = lines.get(i);
                String currentLine = line; // Verwenden Sie eine lokale Variable, um die Zeile zu ändern

                Matcher titleMatcher = TITLE_PATTERN.matcher(currentLine);
                if (titleMatcher.find() && titleMatcher.group(1).trim().equals(title)) {
                    documentFound = true;
                }
                
                if (documentFound) {
                    
                    // Score Update Logic
                    if (!scoreUpdated) {
                        Matcher scoreMatcher = SCORE_PATTERN.matcher(currentLine);
                        if (scoreMatcher.find()) {
                            try {
                                int oldScore = Integer.parseInt(scoreMatcher.group(1));
                                int newScore = Math.min(oldScore + 5, 100); 
                                
                                currentLine = "SCORE: " + newScore;
                                scoreUpdated = true;
                            } catch (NumberFormatException e) {
                                // Ignore malformed score line
                            }
                        }
                    }
                    
                    // Tags Update Logic
                    if (!tagsUpdated) {
                        Matcher tagsMatcher = TAGS_PATTERN.matcher(currentLine);
                        if (tagsMatcher.find()) {
                            String existingTags = tagsMatcher.group(1).trim();
                            // Prüfen, ob der Tag nicht schon existiert, um Duplikate zu verhindern
                            if (!existingTags.contains(newTag)) {
                                currentLine = "TAGS: " + existingTags + ", " + newTag;
                                tagsUpdated = true;
                            }
                        }
                    }
                }
                
                newLines.add(currentLine);
                
                if (line.equals(DOC_SEPARATOR.trim())) {
                    documentFound = false;
                    scoreUpdated = false;
                    tagsUpdated = false; // Reset flags
                }
            }
            
            // KORRIGIERTE PRÜFUNG: Matcher wird nur einmal pro Zeile erstellt und find() wird vor group(1) aufgerufen.
            boolean titleFoundAfterUpdate = newLines.stream().anyMatch(line -> {
                Matcher m = TITLE_PATTERN.matcher(line);
                return m.find() && m.group(1).trim().equals(title);
            });
            
            if (titleFoundAfterUpdate) {
                 Files.write(Paths.get(KNOWLEDGE_FILE), newLines, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
                 loadDocuments(); 
                 return "SCORE-UPDATE: Score für Dokument '" + title + "' um +5 Punkte erhöht. Suchbegriff ('" + searchQuery.trim() + "') als neuen TAG hinzugefügt.";
            } else {
                 return "FEHLER: Dokument '" + title + "' konnte nicht gefunden oder Score nicht aktualisiert werden.";
            }
            
        } catch (IOException e) {
            e.printStackTrace();
            return "FEHLER: Dateioperation fehlgeschlagen beim Aktualisieren des Scores.";
        }
    }
    
    public List<Map<String, String>> loadDocumentsForEditing() {
        return documents.stream()
            .map(doc -> {
                Map<String, String> docMap = new HashMap<>();
                
                Matcher titleM = TITLE_PATTERN.matcher(doc);
                String title = titleM.find() ? titleM.group(1).trim() : "UNBEKANNT";

                Matcher tagsM = TAGS_PATTERN.matcher(doc);
                String tags = tagsM.find() ? tagsM.group(1).trim() : "";
                
                Matcher scoreM = SCORE_PATTERN.matcher(doc);
                String score = scoreM.find() ? scoreM.group(1).trim() : "0";

                String content = doc.replaceAll(TITLE_PATTERN.pattern() + "\n", "")
                                    .replaceAll(TAGS_PATTERN.pattern() + "\n", "")
                                    .replaceAll(SCORE_PATTERN.pattern() + "\n", "")
                                    .trim();

                docMap.put("title", title);
                docMap.put("tags", tags);
                docMap.put("score", score);
                docMap.put("content", content);
                return docMap;
            })
            .collect(Collectors.toList());
    }

    public void saveAllDocuments(List<Map<String, String>> updatedDocs) {
        StringBuilder sb = new StringBuilder();
        for (Map<String, String> doc : updatedDocs) {
            sb.append("TITEL: ").append(doc.get("title")).append("\n");
            sb.append("TAGS: ").append(doc.get("tags")).append("\n");
            sb.append("SCORE: ").append(doc.get("score")).append("\n");
            sb.append(doc.get("content")).append("\n");
            sb.append(DOC_SEPARATOR);
        }
        
        try {
            Files.write(Paths.get(KNOWLEDGE_FILE), sb.toString().getBytes(StandardCharsets.UTF_8), 
                        StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
            loadDocuments(); 
        } catch (IOException e) {
            System.err.println("Fehler beim Speichern der Wissensdatei: " + e.getMessage());
        }
    }

    public void saveNewDocument(String title, String tags, String content) {
        String newDoc = String.format("TITEL: %s\nTAGS: %s\nSCORE: 0\n%s\n%s", title, tags, content, DOC_SEPARATOR);
        try {
            Files.write(Paths.get(KNOWLEDGE_FILE), newDoc.getBytes(StandardCharsets.UTF_8), 
                        StandardOpenOption.CREATE, StandardOpenOption.APPEND);
            loadDocuments(); 
        } catch (IOException e) {
            System.err.println("Fehler beim Speichern des neuen Dokuments: " + e.getMessage());
        }
    }
}
