package de.frigosped.dc.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import de.frigosped.dc.client.OrdsClient;
import de.frigosped.dc.model.*;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.output.Response;
import dev.langchain4j.data.message.AiMessage;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.ws.rs.core.Response.Status;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;

import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Generiert einen Fragenkatalog aus einem Regelwerk-Dokument (z.B. ADSp, AGB).
 *
 * Chunked-Ansatz: Das Dokument wird in Abschnitte (~15k Zeichen) aufgeteilt.
 * Pro Abschnitt wird ein KI-Aufruf gemacht (kürzerer Kontext → kein Timeout).
 * Die Ergebnisse werden anschließend nach Kategoriename zusammengeführt.
 *
 * Asynchroner Ablauf:
 *   1. startGeneration()  → legt Katalog-Placeholder (GENERATING) in DB an, startet Background-Task
 *   2. runGeneration()    → OCR/Textextraktion → Chunking → KI pro Chunk → Merge → Persistierung
 *   3. Status in DB       → READY (Erfolg) oder ERROR (Fehler)
 */
@ApplicationScoped
public class CatalogGenerationService {

    private static final Logger LOG = Logger.getLogger(CatalogGenerationService.class);

    private static final int MAX_TEXT_CHARS = 120_000;
    private static final int CHUNK_SIZE     = 15_000;

    private static final Pattern JSON_PATTERN =
            Pattern.compile("```(?:json)?\\s*([\\s\\S]*?)```|([{][\\s\\S]*[}])",
                    Pattern.DOTALL);

    @Inject
    DocumentProcessingService documentProcessingService;

    @Inject
    @RestClient
    OrdsClient ordsClient;

    @Inject
    @Named("catalog")
    ChatLanguageModel catalogModel;

    @Inject
    AiCostTrackerService costTracker;

    @Inject
    ObjectMapper objectMapper;

    // =========================================================================
    // Öffentliche API – sofortige Rückgabe (202-Muster)
    // =========================================================================

    public long startGeneration(byte[] fileBytes,
                                String mimeType,
                                String filename,
                                String catalogName,
                                String description,
                                long documentTypeId) {

        long catalogId = createCatalogPlaceholder(catalogName, description, documentTypeId);
        LOG.infof("Katalog-Placeholder angelegt (ID %d), starte Hintergrund-Generierung für '%s'",
                catalogId, catalogName);

        CompletableFuture.runAsync(
                () -> runGeneration(catalogId, fileBytes, mimeType, filename, catalogName));

        return catalogId;
    }

    // =========================================================================
    // Hintergrund-Verarbeitung
    // =========================================================================

    private void runGeneration(long catalogId, byte[] fileBytes,
                               String mimeType, String filename, String catalogName) {
        try {
            LOG.infof("[Katalog %d] Starte Textextraktion aus '%s'...", catalogId, filename);
            String rawText = documentProcessingService.extractText(fileBytes, mimeType, filename);
            LOG.infof("[Katalog %d] Textextraktion abgeschlossen: %d Zeichen", catalogId, rawText.length());

            String documentText = truncateIfNeeded(catalogId, rawText);
            List<String> chunks  = splitIntoChunks(documentText, CHUNK_SIZE);
            LOG.infof("[Katalog %d] Dokument in %d Chunk(s) à max. %d Zeichen aufgeteilt",
                    catalogId, chunks.size(), CHUNK_SIZE);

            // Pro Chunk: Kategorien+Fragen extrahieren, nach Kategoriename mergen
            Map<String, GeneratedCatalogStructure.GeneratedCategory> categoryMap = new LinkedHashMap<>();

            for (int i = 0; i < chunks.size(); i++) {
                int chunkNr   = i + 1;
                int totalChunks = chunks.size();
                LOG.infof("[Katalog %d] Chunk %d/%d an KI senden (%d Zeichen)...",
                        catalogId, chunkNr, totalChunks, chunks.get(i).length());

                GeneratedCatalogStructure chunkResult =
                        generateStructureFromChunk(catalogName, chunks.get(i), chunkNr, totalChunks, catalogId);

                if (chunkResult == null || chunkResult.getCategories() == null) {
                    LOG.warnf("[Katalog %d] Chunk %d lieferte kein Ergebnis – übersprungen",
                            catalogId, chunkNr);
                    continue;
                }

                mergeInto(categoryMap, chunkResult.getCategories());
                LOG.infof("[Katalog %d] Chunk %d/%d fertig – %d Kategorien bisher gesamt",
                        catalogId, chunkNr, totalChunks, categoryMap.size());
            }

            if (categoryMap.isEmpty()) {
                throw new IllegalStateException("KI hat für keinen Chunk Kategorien generiert.");
            }

            GeneratedCatalogStructure merged = buildMergedStructure(categoryMap);

            int totalQuestions = merged.getCategories().stream()
                    .mapToInt(c -> c.getQuestions() == null ? 0 : c.getQuestions().size())
                    .sum();
            LOG.infof("[Katalog %d] Merge abgeschlossen: %d Kategorien, %d Fragen",
                    catalogId, merged.getCategories().size(), totalQuestions);

            persistCategories(catalogId, merged);
            updateStatus(catalogId, "READY", null);

        } catch (Exception e) {
            LOG.errorf(e, "[Katalog %d] Generierung fehlgeschlagen", catalogId);
            updateStatus(catalogId, "ERROR",
                    abbreviate(e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName(), 3900));
        }
    }

    // =========================================================================
    // Chunk-Splitting
    // =========================================================================

    private List<String> splitIntoChunks(String text, int maxChunkSize) {
        if (text.length() <= maxChunkSize) return List.of(text);

        List<String> chunks = new ArrayList<>();
        int start = 0;

        while (start < text.length()) {
            int end = Math.min(start + maxChunkSize, text.length());

            // Nicht mitten in einem Paragraph trennen – rückwärts nach Doppel-Newline suchen
            if (end < text.length()) {
                int boundary = text.lastIndexOf("\n\n", end);
                if (boundary > start + maxChunkSize / 2) {
                    end = boundary + 2;
                } else {
                    // Fallback: Satzende suchen
                    int sentEnd = Math.max(
                            text.lastIndexOf(". ", end),
                            text.lastIndexOf(".\n", end));
                    if (sentEnd > start + maxChunkSize / 2) {
                        end = sentEnd + 2;
                    }
                }
            }

            chunks.add(text.substring(start, end).strip());
            start = end;
        }

        return chunks;
    }

    // =========================================================================
    // Merge: Kategorien nach normalisiertem Namen zusammenführen
    // =========================================================================

    private void mergeInto(Map<String, GeneratedCatalogStructure.GeneratedCategory> categoryMap,
                           List<GeneratedCatalogStructure.GeneratedCategory> newCategories) {
        for (var cat : newCategories) {
            if (cat.getName() == null || cat.getName().isBlank()) continue;
            String key = normalizeKey(cat.getName());

            if (categoryMap.containsKey(key)) {
                var existing = categoryMap.get(key);
                List<GeneratedCatalogStructure.GeneratedQuestion> merged =
                        new ArrayList<>(existing.getQuestions() != null ? existing.getQuestions() : List.of());
                if (cat.getQuestions() != null) merged.addAll(cat.getQuestions());
                existing.setQuestions(merged);
            } else {
                // Mutable List sicherstellen
                if (cat.getQuestions() != null) {
                    cat.setQuestions(new ArrayList<>(cat.getQuestions()));
                }
                categoryMap.put(key, cat);
            }
        }
    }

    private GeneratedCatalogStructure buildMergedStructure(
            Map<String, GeneratedCatalogStructure.GeneratedCategory> categoryMap) {

        List<GeneratedCatalogStructure.GeneratedCategory> cats = new ArrayList<>(categoryMap.values());

        // Reihenfolge und Nummerierung normalisieren
        for (int i = 0; i < cats.size(); i++) {
            cats.get(i).setOrderNr(i + 1);
            List<GeneratedCatalogStructure.GeneratedQuestion> qs = cats.get(i).getQuestions();
            if (qs != null) {
                for (int j = 0; j < qs.size(); j++) {
                    fillQuestionDefaults(qs.get(j), j + 1);
                }
            }
        }

        var structure = new GeneratedCatalogStructure();
        structure.setCategories(cats);
        return structure;
    }

    private String normalizeKey(String name) {
        return name.toLowerCase(Locale.GERMAN)
                   .replaceAll("[^a-z0-9äöüß ]", "")
                   .trim()
                   .replaceAll("\\s+", " ");
    }

    // =========================================================================
    // Status-Updates via ORDS
    // =========================================================================

    private void updateStatus(long catalogId, String status, String error) {
        try {
            ordsClient.updateCatalogStatus(catalogId,
                    new OrdsCatalogStatusUpdateRequest(status, error));
            LOG.infof("[Katalog %d] Status → %s", catalogId, status);
        } catch (Exception e) {
            LOG.errorf(e, "[Katalog %d] Status-Update auf '%s' fehlgeschlagen", catalogId, status);
        }
    }

    // =========================================================================
    // ORDS: Katalog-Placeholder anlegen
    // =========================================================================

    private long createCatalogPlaceholder(String name, String description, long documentTypeId) {
        var request  = new OrdsCatalogCreateRequest(name, description, documentTypeId);
        var response = ordsClient.createCatalog(request);
        String body  = response.readEntity(String.class);

        if (response.getStatus() != Status.CREATED.getStatusCode()) {
            throw new RuntimeException(
                    "ORDS Katalog-Erstellung fehlgeschlagen (HTTP "
                    + response.getStatus() + "): " + body);
        }
        return extractId(body, "Katalog");
    }

    // =========================================================================
    // ORDS: Kategorien und Fragen persistieren
    // =========================================================================

    private void persistCategories(long catalogId, GeneratedCatalogStructure structure) {
        for (var category : structure.getCategories()) {
            long categoryId = createCategory(catalogId, category);
            if (category.getQuestions() == null) continue;
            for (var question : category.getQuestions()) {
                createQuestion(categoryId, question);
            }
        }
    }

    private long createCategory(long catalogId,
                                GeneratedCatalogStructure.GeneratedCategory cat) {
        var request  = new OrdsCategoryCreateRequest(cat.getName(), cat.getDescription(), cat.getOrderNr());
        var response = ordsClient.createCategory(catalogId, request);
        String body  = response.readEntity(String.class);

        if (response.getStatus() != Status.CREATED.getStatusCode()) {
            throw new RuntimeException(
                    "ORDS Kategorie-Erstellung fehlgeschlagen (HTTP "
                    + response.getStatus() + "): " + body);
        }
        return extractId(body, "Kategorie '" + cat.getName() + "'");
    }

    private void createQuestion(long categoryId,
                                GeneratedCatalogStructure.GeneratedQuestion q) {
        var request = new OrdsQuestionCreateRequest();
        request.setQuestionText(q.getQuestionText());
        request.setEvaluationType(q.getEvaluationType());
        request.setThreshold(q.getThreshold());
        request.setResultHandling(q.getResultHandling());
        request.setExample0Percent(q.getExample0Percent());
        request.setExample100Percent(q.getExample100Percent());
        request.setOrderNr(q.getOrderNr());

        var response = ordsClient.createQuestion(categoryId, request);
        if (response.getStatus() != Status.CREATED.getStatusCode()) {
            String body = response.readEntity(String.class);
            LOG.warnf("Frage konnte nicht angelegt werden (HTTP %d): %s",
                    response.getStatus(), body);
        }
    }

    // =========================================================================
    // KI-Aufruf pro Chunk
    // =========================================================================

    private GeneratedCatalogStructure generateStructureFromChunk(String catalogName,
                                                                  String chunkText,
                                                                  int chunkNr,
                                                                  int totalChunks,
                                                                  long catalogId) {
        List<ChatMessage> messages = List.of(
            SystemMessage.from(buildChunkSystemPrompt(chunkNr, totalChunks)),
            UserMessage.from(
                "## Regelwerk: " + catalogName
                + " (Abschnitt " + chunkNr + " von " + totalChunks + ")\n\n"
                + chunkText
                + "\n\n---\n\nErstelle jetzt den Fragenkatalog-Ausschnitt als JSON:")
        );

        Response<AiMessage> response = catalogModel.generate(messages);
        costTracker.track("catalog", "CATALOG", null, catalogId, response.tokenUsage());
        String rawJson = response.content().text();
        return parseCatalogStructure(rawJson, catalogName + " Chunk " + chunkNr);
    }

    private String buildChunkSystemPrompt(int chunkNr, int totalChunks) {
        return """
            Du bist ein Experte für Transportrecht, AGB-Analyse und Dokumentenprüfung.

            AUFGABE: Erstelle aus dem folgenden ABSCHNITT (%d von %d) eines Regelwerks
            einen Fragenkatalog-Ausschnitt zur Dokumentenprüfung.
            Der Katalog wird verwendet, um Transportdokumente auf Einhaltung des Regelwerks zu prüfen.

            ════════════════════════════════════════
            STRUKTUR (PFLICHT)
            ════════════════════════════════════════
            • Erstelle 2–5 THEMATISCHE Kategorien NUR für diesen Abschnitt.
              Beispiele: "Haftung", "Informationspflichten", "Fristen und Termine".
              NICHT eine Kategorie pro Paragraph.
            • Pro Kategorie: 3–15 EINZELNE, KONKRETE Fragen.
              Jede Frage prüft GENAU EINE Anforderung.
              KEINE Mega-Fragen wie "Sind alle Anforderungen aus §X erfüllt?".
            • Decke ALLE Klauseln/Paragraphen dieses Abschnitts ab.
              Sehr ähnliche Klauseln dürfen zu einer Frage zusammengefasst werden.

            ════════════════════════════════════════
            BEISPIEL einer korrekten Frage (dieses Muster IMMER einhalten):
            ════════════════════════════════════════
            {
              "question_text": "Ist der Haftungshöchstbetrag für Güterschäden auf 8,33 SZR/kg Rohgewicht begrenzt?",
              "evaluation_type": "BINARY",
              "threshold": 80,
              "result_handling": "ABWEICHUNG",
              "example_0_percent": "Dokument enthält keine Haftungsbegrenzung; der Spediteur haftet unbeschränkt nach allgemeinem Deliktsrecht ohne Mengenbezug.",
              "example_100_percent": "Dokument regelt ausdrücklich: Haftung max. 8,33 SZR pro kg Rohgewicht gem. §431 HGB; bei Totalverlust Sendung max. 1,25 Mio. Euro."
            }

            ════════════════════════════════════════
            REGELN FÜR DIE FELDER
            ════════════════════════════════════════
            evaluation_type:
              "SCORE"  → graduell erfüllbar (Vollständigkeit, Klarheit, Umfang)
              "BINARY" → Ja/Nein (Klausel vorhanden oder nicht, Wert genannt oder nicht)

            threshold (0–100):
              80 → haftungsrelevant oder rechtlich verbindlich
              70 → wichtig, aber nicht haftungskritisch
              60 → empfohlen

            result_handling:
              "ABWEICHUNG" → Fehlen hat rechtliche oder vertragliche Konsequenzen
              "HINWEIS"    → empfohlen, kein Pflichtverstoß

            ════════════════════════════════════════
            PFLICHTREGELN FÜR BEISPIELE
            ════════════════════════════════════════
            example_0_percent und example_100_percent sind IMMER auszufüllen.
            Leere Felder oder Texte wie "nicht erfüllt" sind VERBOTEN.

            example_0_percent:  Was fehlt konkret?
              → "Dokument enthält keine Regelung zu [konkretem Merkmal]."
              → "Es fehlt jeder Hinweis auf [konkreten Wert/Klausel/Verweis]."

            example_100_percent: Was ist konkret vorhanden?
              → "Dokument enthält: [Beispielformulierung, Zahlenwert, Paragraph]."
              → "Klar geregelt: [Merkmal] mit [konkretem Wert oder Beispieltext]."

            ════════════════════════════════════════
            AUSGABE
            ════════════════════════════════════════
            Antworte AUSSCHLIESSLICH mit einem gültigen JSON-Objekt.
            Kein Text, keine Erklärung davor oder danach.

            {
              "categories": [
                {
                  "name": "Kategoriename (max. 80 Zeichen)",
                  "description": "Welche Paragraphen/Themen diese Kategorie abdeckt",
                  "order_nr": 1,
                  "questions": [
                    {
                      "question_text": "...",
                      "evaluation_type": "SCORE",
                      "threshold": 70,
                      "result_handling": "ABWEICHUNG",
                      "example_0_percent": "...",
                      "example_100_percent": "...",
                      "order_nr": 1
                    }
                  ]
                }
              ]
            }
            """.formatted(chunkNr, totalChunks);
    }

    // =========================================================================
    // JSON-Parsing
    // =========================================================================

    private GeneratedCatalogStructure parseCatalogStructure(String rawJson, String context) {
        try {
            String json = extractJson(rawJson);
            GeneratedCatalogStructure structure =
                    objectMapper.readValue(json, GeneratedCatalogStructure.class);

            if (structure.getCategories() != null) {
                for (int i = 0; i < structure.getCategories().size(); i++) {
                    var cat = structure.getCategories().get(i);
                    if (cat.getOrderNr() == 0) cat.setOrderNr(i + 1);
                    if (cat.getQuestions() != null) {
                        for (int j = 0; j < cat.getQuestions().size(); j++) {
                            fillQuestionDefaults(cat.getQuestions().get(j), j + 1);
                        }
                    }
                }
            }
            return structure;

        } catch (Exception e) {
            LOG.errorf("JSON-Parsing fehlgeschlagen für '%s': %s", context, e.getMessage());
            throw new RuntimeException("KI-Antwort konnte nicht geparst werden: " + e.getMessage(), e);
        }
    }

    private void fillQuestionDefaults(GeneratedCatalogStructure.GeneratedQuestion q, int orderNr) {
        if (q.getOrderNr() == 0)           q.setOrderNr(orderNr);
        if (q.getEvaluationType() == null)  q.setEvaluationType("SCORE");
        if (q.getThreshold() == null)       q.setThreshold(70);
        if (q.getResultHandling() == null)  q.setResultHandling("ABWEICHUNG");
        if (isBlank(q.getExample0Percent()))
            q.setExample0Percent(
                "Dokument enthält keine Regelung zur Anforderung: "
                + abbreviate(q.getQuestionText(), 120));
        if (isBlank(q.getExample100Percent()))
            q.setExample100Percent(
                "Anforderung vollständig und klar erfüllt: "
                + abbreviate(q.getQuestionText(), 120));
    }

    private String extractJson(String text) {
        if (text == null || text.isBlank())
            throw new IllegalArgumentException("Leere KI-Antwort");

        Matcher m = JSON_PATTERN.matcher(text);
        if (m.find()) {
            String candidate = m.group(1) != null ? m.group(1) : m.group(2);
            if (candidate != null && !candidate.isBlank()) return candidate.trim();
        }
        int start = text.indexOf('{');
        int end   = text.lastIndexOf('}');
        if (start >= 0 && end > start) return text.substring(start, end + 1);
        throw new IllegalArgumentException("Kein JSON in KI-Antwort gefunden");
    }

    // =========================================================================
    // Hilfsmethoden
    // =========================================================================

    private String truncateIfNeeded(long catalogId, String text) {
        if (text.length() <= MAX_TEXT_CHARS) return text;
        LOG.warnf("[Katalog %d] Text (%d Zeichen) überschreitet Limit – kürze auf %d",
                catalogId, text.length(), MAX_TEXT_CHARS);
        return text.substring(0, MAX_TEXT_CHARS)
                + "\n\n[... Dokument wurde aus Kapazitätsgründen gekürzt ...]";
    }

    @SuppressWarnings("unchecked")
    private long extractId(String responseBody, String context) {
        try {
            Map<String, Object> map = objectMapper.readValue(responseBody, Map.class);
            Object id = map.get("id");
            if (id instanceof Number n) return n.longValue();
            throw new IllegalStateException("Kein 'id'-Feld in ORDS-Antwort");
        } catch (Exception e) {
            throw new RuntimeException(
                    "ID-Parsing fehlgeschlagen für " + context + ": " + responseBody, e);
        }
    }

    private String abbreviate(String s, int maxLen) {
        if (s == null) return "";
        return s.length() <= maxLen ? s : s.substring(0, maxLen) + "…";
    }

    private boolean isBlank(String s) {
        return s == null || s.isBlank();
    }
}
