package de.frigosped.dc.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import de.frigosped.dc.model.EvaluationResult;
import de.frigosped.dc.model.OrdsQuestion;
import dev.langchain4j.data.message.AiMessage;
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 jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.jboss.logging.Logger;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * KI-gestützte Auswertung:
 *   1. Übersetzung eines Dokuments ins Deutsche
 *   2. Prüfung des Dokuments gegen eine einzelne Frage
 *
 * Verwendet das Hauptmodell (gpt-oss:20b).
 */
@ApplicationScoped
public class EvaluationService {

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

    /** Regex zum Extrahieren von JSON aus der KI-Antwort (toleriert Markdown-Codeblöcke) */
    private static final Pattern JSON_PATTERN =
            Pattern.compile("```(?:json)?\\s*([\\s\\S]*?)```|([{][\\s\\S]*[}])",
                    Pattern.DOTALL);

    @Inject
    @Named("main")
    ChatLanguageModel mainModel;

    @Inject
    AiCostTrackerService costTracker;

    @Inject
    ObjectMapper objectMapper;

    // =========================================================================
    // Übersetzung
    // =========================================================================

    /**
     * Übersetzt einen Markdown-Text aus der Quellsprache ins Deutsche.
     * Wenn der Text bereits auf Deutsch ist, wird er unverändert zurückgegeben.
     *
     * @param originalText  Markdown-Text in Originalsprache
     * @param projectId     Für Kostenzuordnung
     * @return              Markdown-Text auf Deutsch
     */
    public String translate(String originalText, long projectId) {
        if (originalText == null || originalText.isBlank()) return "";
        LOG.debugf("Starte Übersetzung (%d Zeichen)...", originalText.length());

        List<ChatMessage> messages = List.of(
            SystemMessage.from(
                "Du bist ein professioneller Übersetzer für rechtliche und logistische Dokumente.\n" +
                "Übersetze den folgenden Text ins Deutsche.\n" +
                "Regeln:\n" +
                "- Behalte die Markdown-Formatierung exakt bei\n" +
                "- Fachbegriffe korrekt übersetzen\n" +
                "- Wenn der Text bereits auf Deutsch ist, gib ihn unverändert zurück\n" +
                "- Antworte NUR mit dem übersetzten Text, ohne Kommentar oder Präambel"
            ),
            UserMessage.from(originalText)
        );

        Response<AiMessage> response = mainModel.generate(messages);
        costTracker.track("main", "TRANSLATE", projectId, null, response.tokenUsage());
        return stripCodeFence(response.content().text().trim());
    }

    /**
     * Entfernt eine umschließende ```-Markierung (z. B. ```markdown), die das
     * Modell der Übersetzung manchmal trotz Anweisung hinzufügt. So wird der
     * Markdown-Text einheitlich ohne Code-Fence gespeichert.
     */
    private String stripCodeFence(String text) {
        if (text == null) return "";
        String t = text.trim();
        if (t.startsWith("```")) {
            int firstNewline = t.indexOf('\n');
            if (firstNewline > 0) {
                t = t.substring(firstNewline + 1);   // erste Zeile (```markdown) entfernen
            }
            if (t.endsWith("```")) {
                t = t.substring(0, t.length() - 3);  // schließendes ``` entfernen
            }
        }
        return t.trim();
    }

    // =========================================================================
    // Referenz-Erkennung
    // =========================================================================

    /**
     * Lässt das Hauptmodell die relevanten externen Dokument-Referenz-URLs direkt
     * aus dem Dokumenttext ausgeben (kein Regex). Login-, Video-, Impressum- und
     * Portalseiten werden anhand des Kontexts herausgefiltert.
     *
     * @return Liste der vom Modell gelieferten URLs (kann leer sein)
     */
    public List<String> extractReferenceUrls(String documentText, long projectId) {
        if (documentText == null || documentText.isBlank()) return List.of();

        List<ChatMessage> messages = List.of(
            SystemMessage.from(
                "Du analysierst Transport- und Speditionsdokumente und findest Verweise auf\n" +
                "EXTERNE Dokumente, die per Link erreichbar sind (z. B. AGB, Allgemeine\n" +
                "Geschäftsbedingungen, Verhaltenskodex/Code of Conduct, Vertragsbedingungen,\n" +
                "Sicherheits- oder Compliance-Richtlinien).\n\n" +
                "Gib NUR Links zurück, die tatsächlich im Text stehen und auf ein solches\n" +
                "Dokument verweisen. Schließe ausdrücklich AUS: Login-/Portalseiten,\n" +
                "Video-Links (z. B. YouTube), Impressum, Datenschutz-Übersichtsseiten,\n" +
                "Hinweisgeber-/Whistleblower-Portale, Social-Media- und reine Startseiten.\n\n" +
                "Verändere die URLs nicht – gib sie exakt so zurück, wie sie im Text stehen.\n\n" +
                "Antworte AUSSCHLIESSLICH mit einem gültigen JSON-Array von Strings (URLs).\n" +
                "Wenn es keine passenden Links gibt, antworte mit []."
            ),
            UserMessage.from("## Dokument\n\n" + documentText + "\n\n---\n\nAntworte mit dem JSON-Array der relevanten URLs:")
        );

        try {
            Response<AiMessage> response = mainModel.generate(messages);
            costTracker.track("main", "REF_EXTRACT", projectId, null, response.tokenUsage());

            String json = extractJsonArray(response.content().text());
            List<String> urls = objectMapper.readValue(
                json,
                objectMapper.getTypeFactory().constructCollectionType(List.class, String.class)
            );
            // Duplikate entfernen, leere Einträge filtern, Reihenfolge erhalten
            return urls.stream()
                    .filter(u -> u != null && !u.isBlank())
                    .map(String::trim)
                    .distinct()
                    .collect(Collectors.toList());
        } catch (Exception e) {
            LOG.warnf("Referenz-URL-Extraktion fehlgeschlagen: %s", e.getMessage());
            return List.of();
        }
    }

    // =========================================================================
    // Fragen-Auswertung
    // =========================================================================

    /**
     * Wertet eine einzelne Frage gegen den Dokument-Text aus.
     *
     * Der Schwellwert (threshold) und die Abweichungs-Logik werden
     * im Orchestrator berechnet – die KI liefert nur score + answer + comment.
     *
     * @param question      Frage aus dem Katalog (inkl. Beispiele, Schwellwert)
     * @param documentText  Originaltext des Dokuments (Markdown)
     * @return              Strukturiertes Auswertungsergebnis
     */
    public EvaluationResult evaluate(OrdsQuestion question, String documentText, long projectId) {
        LOG.debugf("Werte Frage %d aus: %s",
            question.getQuestionId(),
            abbreviate(question.getQuestionText(), 80));

        String systemPrompt = buildEvaluationSystem();
        String userPrompt   = buildEvaluationUser(question, documentText);

        List<ChatMessage> messages = List.of(
            SystemMessage.from(systemPrompt),
            UserMessage.from(userPrompt)
        );

        Response<AiMessage> response = mainModel.generate(messages);
        costTracker.track("main", "EVALUATE", projectId, null, response.tokenUsage());
        String rawText = response.content().text();

        return parseEvaluationResult(rawText, question);
    }

    // =========================================================================
    // Batch-Auswertung (bis zu 4 Fragen pro KI-Aufruf)
    // =========================================================================

    /**
     * Wertet mehrere Fragen in einem einzigen KI-Aufruf aus.
     * Gibt eine Map question_id → EvaluationResult zurück.
     * Wirft eine Exception wenn die KI-Antwort nicht als JSON-Array parsbar ist,
     * damit der Aufrufer auf Einzel-Aufrufe zurückfallen kann.
     */
    public Map<Long, EvaluationResult> evaluateBatch(List<OrdsQuestion> questions,
                                                      String documentText,
                                                      long projectId) {
        if (questions.isEmpty()) return Map.of();

        LOG.debugf("Batch-Auswertung: %d Frage(n)", questions.size());

        List<ChatMessage> messages = List.of(
            SystemMessage.from(buildBatchSystem(questions.size())),
            UserMessage.from(buildBatchUser(questions, documentText))
        );

        Response<AiMessage> response = mainModel.generate(messages);
        costTracker.track("main", "EVALUATE", projectId, null, response.tokenUsage());

        List<EvaluationResult> results = parseBatchResult(response.content().text(), questions);

        Map<Long, EvaluationResult> map = new HashMap<>();
        for (EvaluationResult r : results) {
            if (r.getQuestionId() != null) {
                map.put(r.getQuestionId(), r);
            }
        }
        // Lücken füllen: für fehlende question_ids Fallback-Ergebnis eintragen
        for (OrdsQuestion q : questions) {
            map.computeIfAbsent(q.getQuestionId(), id -> {
                LOG.warnf("Keine Batch-Antwort für Frage %d – verwende UNKLAR-Fallback", id);
                return buildFallback("Frage wurde im Batch nicht beantwortet.");
            });
        }
        return map;
    }

    private String buildBatchSystem(int questionCount) {
        return """
            Du bist ein Experte für Transportrecht, AGB-Recht und Vertragsanalyse.
            Du prüfst Transport- und Speditionsdokumente gegen einen Fragenkatalog.

            Wichtige Suchregel: Suche im Dokument ZUERST nach expliziten, wörtlichen Formulierungen,
            die die Anforderung direkt bestätigen oder verneinen. Zitiere immer die stärkste,
            direkteste Textstelle. Nutze indirekte oder implizite Belege NUR dann, wenn keine
            direkte Formulierung im Dokument existiert.

            Antworte AUSSCHLIESSLICH mit einem gültigen JSON-Array – kein Text davor oder danach.
            Das Array enthält für JEDE Frage genau ein Objekt:

            [
              {
                "question_id": <ID der Frage – exakt wie angegeben>,
                "answer": "<Antwort auf Deutsch; Zitate aus dem Dokument in Originalsprache mit Übersetzung in Klammern>",
                "score": <Erfüllungsgrad 0 bis 100>,
                "result_type": "<OK wenn score >= Schwellwert, NOK wenn klar nicht erfüllt, UNKLAR wenn grenzwertig>",
                "the_comment": "<Kurzer fachlicher Kommentar auf Deutsch, max. 3 Sätze>"
              }
            ]

            Pflicht: Das Array muss GENAU %d Einträge enthalten (einen pro Frage).
            Die question_id jedes Eintrags muss exakt der ID der gestellten Frage entsprechen.
            """.formatted(questionCount);
    }

    private String buildBatchUser(List<OrdsQuestion> questions, String documentText) {
        StringBuilder sb = new StringBuilder();
        sb.append("## Zu prüfendes Dokument\n\n");
        sb.append(documentText).append("\n\n---\n\n");
        sb.append("## Prüffragen (").append(questions.size()).append(" Fragen)\n\n");

        for (int i = 0; i < questions.size(); i++) {
            OrdsQuestion q = questions.get(i);
            sb.append("### Frage ").append(i + 1)
              .append(" (ID: ").append(q.getQuestionId())
              .append(", Kategorie: ").append(q.getCategoryName()).append(")\n\n");
            sb.append(q.getQuestionText()).append("\n\n");

            if (q.getThreshold() != null) {
                sb.append("**Schwellwert:** ").append(q.getThreshold())
                  .append("% – unterhalb gilt die Anforderung als nicht erfüllt\n\n");
            }
            if (q.getExample0Percent() != null && !q.getExample0Percent().isBlank()) {
                sb.append("**Beispiel 0%:** ").append(q.getExample0Percent()).append("\n\n");
            }
            if (q.getExample100Percent() != null && !q.getExample100Percent().isBlank()) {
                sb.append("**Beispiel 100%:** ").append(q.getExample100Percent()).append("\n\n");
            }
        }

        sb.append("---\n\nAntworte jetzt mit dem JSON-Array (").append(questions.size())
          .append(" Einträge):");
        return sb.toString();
    }

    private List<EvaluationResult> parseBatchResult(String rawText, List<OrdsQuestion> questions) {
        try {
            String json = extractJsonArray(rawText);
            List<EvaluationResult> results = objectMapper.readValue(
                json,
                objectMapper.getTypeFactory().constructCollectionType(List.class, EvaluationResult.class)
            );
            // Fehlende Defaults setzen
            for (int i = 0; i < results.size(); i++) {
                EvaluationResult r = results.get(i);
                if (r.getScore() == null) r.setScore(0);
                if (r.getAnswer() == null) r.setAnswer("Keine Antwort erhalten.");
                // question_id aus Position ableiten falls KI sie vergessen hat
                if (r.getQuestionId() == null && i < questions.size()) {
                    r.setQuestionId(questions.get(i).getQuestionId());
                }
                if (r.getResultType() == null || !isValidResultType(r.getResultType())) {
                    Integer threshold = (i < questions.size()) ? questions.get(i).getThreshold() : null;
                    r.setResultType(deriveResultType(r.getScore(), threshold));
                }
            }
            return results;
        } catch (Exception e) {
            LOG.warnf("Batch-JSON-Parsing fehlgeschlagen: %s", e.getMessage());
            LOG.debugf("Rohe KI-Antwort: %s", rawText);
            throw new RuntimeException("Batch-Parsing fehlgeschlagen: " + e.getMessage(), e);
        }
    }

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

        // JSON in ```-Block
        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.stripLeading().startsWith("[")) return candidate.trim();
        }
        // Direkt als Array
        int start = text.indexOf('[');
        int end   = text.lastIndexOf(']');
        if (start >= 0 && end > start) return text.substring(start, end + 1);

        throw new IllegalArgumentException("Kein JSON-Array in KI-Antwort gefunden");
    }

    private EvaluationResult buildFallback(String message) {
        EvaluationResult r = new EvaluationResult();
        r.setAnswer(message);
        r.setScore(0);
        r.setResultType("UNKLAR");
        r.setTheComment("Automatische Auswertung konnte nicht zugeordnet werden.");
        return r;
    }

    // =========================================================================
    // Prompt-Builder
    // =========================================================================

    private String buildEvaluationSystem() {
        return """
            Du bist ein Experte für Transportrecht, AGB-Recht und Vertragsanalyse.
            Du prüfst Transport- und Speditionsdokumente gegen einen Fragenkatalog.

            Wichtige Suchregel: Suche im Dokument ZUERST nach expliziten, wörtlichen Formulierungen,
            die die Anforderung direkt bestätigen oder verneinen. Zitiere immer die stärkste,
            direkteste Textstelle. Nutze indirekte oder implizite Belege NUR dann, wenn keine
            direkte Formulierung im Dokument existiert.

            Antworte AUSSCHLIESSLICH mit einem gültigen JSON-Objekt – kein Text davor oder danach.
            Das JSON-Objekt muss folgende Felder enthalten:

            {
              "answer": "<Deine Antwort auf Deutsch. Zitate aus dem Dokument in Originalsprache angeben und danach in Klammern übersetzen>",
              "score": <Zahl 0 bis 100, wie gut das Dokument die Anforderung erfüllt>,
              "result_type": "<OK wenn score >= Schwellwert, UNKLAR wenn grenzwertig, NOK wenn klar nicht erfüllt>",
              "the_comment": "<Kurzer fachlicher Kommentar auf Deutsch, max. 3 Sätze>"
            }
            """;
    }

    private String buildEvaluationUser(OrdsQuestion question, String documentText) {
        StringBuilder sb = new StringBuilder();

        sb.append("## Zu prüfendes Dokument\n\n");
        sb.append(documentText).append("\n\n");

        sb.append("---\n\n");
        sb.append("## Prüffrage (Kategorie: ").append(question.getCategoryName()).append(")\n\n");
        sb.append(question.getQuestionText()).append("\n\n");

        if (question.getThreshold() != null) {
            sb.append("**Schwellwert:** ").append(question.getThreshold())
              .append("% – unterhalb gilt die Anforderung als nicht erfüllt\n\n");
        }

        if (question.getExample0Percent() != null && !question.getExample0Percent().isBlank()) {
            sb.append("**Beispiel: 0% Erfüllung (nicht ok):**\n")
              .append(question.getExample0Percent()).append("\n\n");
        }

        if (question.getExample100Percent() != null && !question.getExample100Percent().isBlank()) {
            sb.append("**Beispiel: 100% Erfüllung (vollständig ok):**\n")
              .append(question.getExample100Percent()).append("\n\n");
        }

        sb.append("---\n\nAntworte jetzt mit dem JSON-Objekt:");
        return sb.toString();
    }

    // =========================================================================
    // JSON-Parsing mit Fallback
    // =========================================================================

    /**
     * Parst das KI-Ergebnis zu einem EvaluationResult.
     * Toleriert Markdown-Codeblöcke und leichte Formatierungsfehler.
     */
    private EvaluationResult parseEvaluationResult(String rawText, OrdsQuestion question) {
        try {
            String json = extractJson(rawText);
            // Jackson liest snake_case dank quarkus.jackson.property-naming-strategy=SNAKE_CASE
            EvaluationResult result = objectMapper.readValue(json, EvaluationResult.class);

            // Validierung + Defaults
            if (result.getScore() == null) result.setScore(0);
            if (result.getResultType() == null || !isValidResultType(result.getResultType())) {
                result.setResultType(deriveResultType(result.getScore(), question.getThreshold()));
            }
            if (result.getAnswer() == null) result.setAnswer("Keine Antwort erhalten.");

            return result;

        } catch (Exception e) {
            LOG.warnf("JSON-Parsing fehlgeschlagen für Frage %d, verwende Fallback. Fehler: %s",
                question.getQuestionId(), e.getMessage());
            LOG.debugf("Rohe KI-Antwort: %s", rawText);

            // Fallback: UNKLAR mit dem Rohtext als Antwort
            EvaluationResult fallback = new EvaluationResult();
            fallback.setAnswer(rawText != null ? rawText.trim() : "Parsing-Fehler");
            fallback.setScore(0);
            fallback.setResultType("UNKLAR");
            fallback.setTheComment("Automatische Auswertung konnte nicht strukturiert werden.");
            return fallback;
        }
    }

    /**
     * Extrahiert den JSON-Teil aus der KI-Antwort
     * (toleriert ```json ... ``` Blöcke oder reines JSON).
     */
    private String extractJson(String text) {
        if (text == null || text.isBlank()) {
            throw new IllegalArgumentException("Leere KI-Antwort");
        }

        // Versuch 1: JSON in ```-Block
        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();
            }
        }

        // Versuch 2: Alles ab erstem { bis letztem }
        int start = text.indexOf('{');
        int end   = text.lastIndexOf('}');
        if (start >= 0 && end > start) {
            return text.substring(start, end + 1);
        }

        throw new IllegalArgumentException("Kein JSON-Objekt in KI-Antwort gefunden");
    }

    private boolean isValidResultType(String type) {
        return "OK".equals(type) || "UNKLAR".equals(type) || "NOK".equals(type);
    }

    private String deriveResultType(int score, Integer threshold) {
        if (threshold == null) threshold = 70;
        if (score >= threshold)            return "OK";
        if (score >= threshold / 2)        return "UNKLAR";
        return "NOK";
    }

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