package de.frigosped.dc.service;

import de.frigosped.dc.client.OrdsClient;
import de.frigosped.dc.model.*;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;

import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * Orchestriert den kompletten Prüf-Ablauf für ein Projekt:
 *
 *   1. Projekt laden → IN_PROGRESS setzen
 *   2. Dokumente herunterladen → OCR / Konvertierung → Texte speichern
 *   3. Texte ins Deutsche übersetzen → gespeichert
 *   4. Fragenkatalog für jeden Dokument-Text auswerten (Reihenfolge: question_id aufsteigend)
 *   5. Ergebnisse Frage × Dokument in DB speichern
 *   6. Projekt auf COMPLETED setzen
 *
 * Wird von CheckResource.startCheck() asynchron (CompletableFuture) aufgerufen.
 */
@ApplicationScoped
public class CheckOrchestrationService {

    private static final Logger LOG = Logger.getLogger(CheckOrchestrationService.class);
    private static final DateTimeFormatter ISO_FMT =
            DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");

    @RestClient
    OrdsClient ordsClient;

    @Inject
    DocumentProcessingService documentProcessingService;

    @Inject
    EvaluationService evaluationService;

    @Inject
    MarkdownReportBuilder markdownReportBuilder;

    @Inject
    MarkdownExportService markdownExportService;

    @Inject
    ReferenceDownloadService referenceDownloadService;

    @Inject
    com.fasterxml.jackson.databind.ObjectMapper objectMapper;

    @org.eclipse.microprofile.config.inject.ConfigProperty(
            name = "dc.references.max-per-document", defaultValue = "5")
    int maxReferencesPerDocument;

    // =========================================================================
    // Haupt-Einstiegspunkt
    // =========================================================================

    public void process(long projectId) {
        LOG.infof("=== Starte Verarbeitung Projekt %d ===", projectId);

        try {
            // 1. Projekt laden
            OrdsProject project = ordsClient.getProject(projectId);
            LOG.infof("Projekt: '%s', Status: %s", project.getName(), project.getStatus());

            // Nur abbrechen wenn bereits aktiv oder fertig (null = neues Projekt)
            String status = project.getStatus();
            if ("IN_PROGRESS".equals(status) || "COMPLETED".equals(status)) {
                LOG.warnf("Projekt %d ist bereits %s – Abbruch", projectId, status);
                return;
            }

            // 2. Status → IN_PROGRESS (setzt auch processing_started_at in ORDS)
            Response startResp = ordsClient.startProject(projectId);
            if (startResp.getStatus() == 409) {
                LOG.warnf("Projekt %d konnte nicht gestartet werden (409) – Abbruch", projectId);
                return;
            }
            Instant startedAt = Instant.now();

            // 3. Dokumente laden
            List<OrdsDocument> documents = ordsClient.getDocuments(projectId).getItems();

            LOG.infof("Projekt %d: %d Dokument(e)", projectId, documents.size());

            if (documents.isEmpty()) {
                LOG.warnf("Projekt %d hat keine Dokumente – markiere als abgeschlossen", projectId);
                ordsClient.completeProject(projectId, new CompleteProjectRequest("", java.util.Collections.emptyList()));
                return;
            }

            // 4. Fragen pro Katalog laden (Katalog ist am Dokument, nicht am Projekt)
            //    Fragen werden nach question_id aufsteigend sortiert.
            Map<Long, List<OrdsQuestion>> questionsByCatalog = new HashMap<>();
            for (OrdsDocument doc : documents) {
                Long catId = doc.getCatalogId();
                if (catId != null && !questionsByCatalog.containsKey(catId)) {
                    List<OrdsQuestion> qs = ordsClient.getQuestions(catId).getItems()
                            .stream()
                            .sorted(Comparator
                                .comparingInt((OrdsQuestion q) ->
                                    q.getCategoryOrderNr() != null ? q.getCategoryOrderNr() : Integer.MAX_VALUE)
                                .thenComparingInt(q ->
                                    q.getOrderNr() != null ? q.getOrderNr() : Integer.MAX_VALUE))
                            .collect(Collectors.toList());
                    questionsByCatalog.put(catId, qs);
                    LOG.infof("Katalog %d: %d Frage(n) geladen", catId, qs.size());
                }
            }

            // 5. Fortschrittsberechnung: 1 Schritt OCR + Anzahl Fragen des Dokument-Katalogs
            int totalSteps = 0;
            for (OrdsDocument doc : documents) {
                Long catId = doc.getCatalogId();
                int qCount = catId != null && questionsByCatalog.containsKey(catId)
                        ? questionsByCatalog.get(catId).size() : 0;
                totalSteps += 1 + qCount;
            }
            int[] stepHolder = {0};

            // 6. Alte Ergebnisse + heruntergeladene Referenzen löschen (ermöglicht Re-Processing)
            Response delResp = ordsClient.deleteResults(projectId);
            LOG.debugf("Alte Ergebnisse gelöscht: HTTP %d", delResp.getStatus());
            try {
                Response delRefResp = ordsClient.deleteReferences(projectId);
                LOG.debugf("Alte Referenzen gelöscht: HTTP %d", delRefResp.getStatus());
            } catch (Exception e) {
                LOG.debugf("Referenzen löschen fehlgeschlagen: %s", e.getMessage());
            }

            // Texte und Ergebnisse sammeln (in-memory für Markdown-Report)
            Map<Long, String> originalTexts = new HashMap<>();
            Map<Long, List<ResultRequest>> resultsByDoc = new HashMap<>();
            // Original-Texte heruntergeladener Referenzen je Hauptdokument-ID
            Map<Long, List<String>> referenceTextsByDoc = new HashMap<>();

            // ─── PHASE A: OCR + Übersetzung (wird übersprungen wenn Text bereits vorhanden) ──
            for (OrdsDocument doc : documents) {
                if (doc.isReference()) continue;   // Referenzen werden während Phase A erzeugt

                pushProgress(projectId, stepHolder[0], totalSteps, "OCR",
                             doc.getId(), null, startedAt);
                pushDocumentProgress(doc.getId(), 0, "IN_PROGRESS");

                String originalText = "";

                if (Integer.valueOf(1).equals(doc.getHasOriginalText())) {
                    LOG.infof("Dokument %d: Text bereits vorhanden – überspringe OCR/Übersetzung",
                        doc.getId());
                    try {
                        OrdsDocumentTexts texts = ordsClient.getTexts(doc.getId());
                        originalText = texts.getOriginalText() != null ? texts.getOriginalText() : "";
                        pushDocumentProgress(doc.getId(), 50, null);
                    } catch (Exception e) {
                        LOG.warnf("Text-Laden für Dokument %d fehlgeschlagen: %s", doc.getId(), e.getMessage());
                    }
                } else {
                    LOG.infof("Verarbeite Dokument %d: %s", doc.getId(), doc.getFilename());
                    String translatedText = "";
                    try {
                        Response fileResp = ordsClient.downloadFile(doc.getId());
                        if (fileResp.getStatus() != 200) {
                            LOG.warnf("Download Dokument %d fehlgeschlagen (HTTP %d)",
                                doc.getId(), fileResp.getStatus());
                            pushDocumentProgress(doc.getId(), null, "ERROR");
                        } else {
                            byte[] fileBytes = fileResp.readEntity(byte[].class);

                            originalText = documentProcessingService.extractText(
                                fileBytes, doc.getMimeType(), doc.getFilename(), projectId);
                            LOG.debugf("Dokument %d: %d Zeichen extrahiert",
                                (long) doc.getId(), (long) originalText.length());
                            pushDocumentProgress(doc.getId(), 25, null);

                            translatedText = evaluationService.translate(originalText, projectId);
                            LOG.debugf("Dokument %d: Übersetzung fertig (%d Zeichen)",
                                (long) doc.getId(), (long) translatedText.length());
                            pushDocumentProgress(doc.getId(), 50, null);

                            ordsClient.updateTexts(doc.getId(),
                                new TextsRequest(originalText, translatedText));
                        }
                    } catch (Exception e) {
                        LOG.errorf(e, "Fehler bei Dokumentenverarbeitung %d", doc.getId());
                        pushDocumentProgress(doc.getId(), null, "ERROR");
                    }
                }

                originalTexts.put(doc.getId(), originalText);

                // Externe Referenzen erkennen, herunterladen und als eigene Zeilen speichern
                if (!originalText.isBlank() && referenceDownloadService.isEnabled()) {
                    try {
                        processReferences(doc, originalText, projectId, referenceTextsByDoc);
                    } catch (Exception e) {
                        LOG.warnf("Referenz-Verarbeitung für Dokument %d fehlgeschlagen: %s",
                                doc.getId(), e.getMessage());
                    }
                }

                stepHolder[0]++;
                pushProgress(projectId, stepHolder[0], totalSteps, "OCR",
                             doc.getId(), null, startedAt);
            }

            // ─── PHASE B: Fragen auswerten ────────────────────────────────
            for (OrdsDocument doc : documents) {
                if (doc.isReference()) continue;   // Referenzen werden nur „in Summe" mitbewertet

                // Auswertung über Haupt- + Referenztext (Option D: „Summe")
                String originalText = buildCombinedText(
                        originalTexts.getOrDefault(doc.getId(), ""),
                        referenceTextsByDoc.get(doc.getId()));
                Long catId = doc.getCatalogId();

                if (catId == null || !questionsByCatalog.containsKey(catId)) {
                    LOG.warnf("Dokument %d hat keinen Katalog – überspringe Auswertung", doc.getId());
                    continue;
                }

                List<OrdsQuestion> questions = questionsByCatalog.get(catId);

                if (originalText.isBlank()) {
                    LOG.warnf("Kein Text für Dokument %d – überspringe Auswertung", doc.getId());
                    stepHolder[0] += questions.size();
                    pushProgress(projectId, stepHolder[0], totalSteps, "QUESTIONS",
                                 doc.getId(), null, startedAt);
                    continue;
                }

                List<List<OrdsQuestion>> batches = partition(questions, 4);
                int batchesDone = 0;

                for (List<OrdsQuestion> batch : batches) {
                    OrdsQuestion first = batch.get(0);
                    pushProgress(projectId, stepHolder[0], totalSteps, "QUESTIONS",
                                 doc.getId(), first.getQuestionId(), startedAt);

                    Map<Long, EvaluationResult> batchResults;
                    try {
                        LOG.debugf("Batch-Auswertung: Dok %d, %d Fragen ab ID %d",
                            (Object) doc.getId(), (Object) batch.size(), (Object) first.getQuestionId());
                        batchResults = evaluationService.evaluateBatch(batch, originalText, projectId);
                    } catch (Exception e) {
                        LOG.warnf("Batch fehlgeschlagen (Dok %d, Frage %d+) – falle auf Einzelaufrufe zurück: %s",
                            doc.getId(), first.getQuestionId(), e.getMessage());
                        batchResults = evaluateIndividually(batch, originalText, projectId);
                    }

                    for (OrdsQuestion question : batch) {
                        EvaluationResult result = batchResults.get(question.getQuestionId());
                        if (result == null) continue;
                        ResultRequest rr = saveResult(projectId, doc.getId(), question, result);
                        if (rr != null) {
                            resultsByDoc.computeIfAbsent(doc.getId(), k -> new ArrayList<>()).add(rr);
                        }
                    }

                    stepHolder[0] += batch.size();
                    batchesDone += batch.size();
                    int docProgress = 50 + (int) Math.min(batchesDone * 50L / questions.size(), 49);
                    pushDocumentProgress(doc.getId(), docProgress, null);
                    pushProgress(projectId, stepHolder[0], totalSteps, "QUESTIONS",
                                 doc.getId(), batch.get(batch.size() - 1).getQuestionId(), startedAt);
                }

                pushDocumentProgress(doc.getId(), 100, "COMPLETED");
            }

            // 7. Kombinierten Markdown-Bericht für die DB generieren
            String reportMarkdown = "";
            try {
                reportMarkdown = markdownReportBuilder.build(
                        project, documents, questionsByCatalog, resultsByDoc);
                LOG.infof("Projekt %d: Markdown-Bericht generiert (%d Zeichen)",
                        projectId, reportMarkdown.length());
            } catch (Exception e) {
                LOG.warnf(e, "Markdown-Generierung fehlgeschlagen – sende leeren Body");
            }

            // 8. Pro Dokument ein eigenes PDF generieren
            Map<Long, OrdsQuestion> questionById = new HashMap<>();
            for (List<OrdsQuestion> qs : questionsByCatalog.values()) {
                for (OrdsQuestion q : qs) questionById.put(q.getQuestionId(), q);
            }

            List<DocumentPdfEntry> documentPdfs = new ArrayList<>();
            for (OrdsDocument doc : documents) {
                List<ResultRequest> docResults =
                        resultsByDoc.getOrDefault(doc.getId(), java.util.Collections.emptyList());
                if (docResults.isEmpty()) continue;
                try {
                    String docMd = markdownReportBuilder.buildForDocument(
                            project, doc, questionById, docResults);
                    byte[] pdfBytes = markdownExportService.toPdf(docMd);
                    String baseName = doc.getFilename().replaceAll("\\.[^.]+$", "");
                    String filename = "Pruefbericht_"
                            + baseName.replaceAll("[^A-Za-z0-9_äöüÄÖÜß.-]", "_")
                            + ".pdf";
                    documentPdfs.add(new DocumentPdfEntry(filename,
                            Base64.getEncoder().encodeToString(pdfBytes)));
                    LOG.infof("Projekt %d: Dok %d – PDF generiert (%d Bytes)",
                            projectId, doc.getId(), pdfBytes.length);
                } catch (Exception e) {
                    LOG.warnf(e, "PDF-Generierung für Dok %d fehlgeschlagen", doc.getId());
                }
            }

            ordsClient.completeProject(projectId,
                    new CompleteProjectRequest(reportMarkdown, documentPdfs));
            LOG.infof("=== Projekt %d erfolgreich abgeschlossen ===", projectId);

        } catch (Exception e) {
            LOG.errorf(e, "=== Kritischer Fehler bei Projekt %d – bleibt IN_PROGRESS ===",
                projectId);
        }
    }

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

    /**
     * Sendet Fortschritt + Schrittdetails + ETA an ORDS.
     * Wird vor (step = bisherige Schritte) und nach (step = bisherige + 1) jedem Schritt aufgerufen.
     */
    private void pushProgress(long projectId, int stepsCompleted, int total,
                              String phase, Long docId, Long questionId, Instant startedAt) {
        int pct = total > 0 ? Math.min(stepsCompleted * 100 / total, 99) : 0;
        String eta = (stepsCompleted > 0 && startedAt != null)
                     ? calcEta(startedAt, stepsCompleted, total) : null;
        try {
            ordsClient.updateProgress(projectId,
                new ProgressRequest(pct, phase, docId, questionId, eta));
        } catch (Exception e) {
            LOG.debugf("Fortschritt konnte nicht gesetzt werden (%d%%): %s", pct, e.getMessage());
        }
    }

    /** Setzt progress und/oder status eines Dokuments. null-Werte werden ignoriert. */
    private void pushDocumentProgress(long docId, Integer progress, String status) {
        try {
            jakarta.ws.rs.core.Response resp =
                    ordsClient.updateDocumentProgress(docId, new DocumentProgressRequest(progress, status));
            if (resp.getStatus() < 200 || resp.getStatus() >= 300) {
                LOG.warnf("Dokument-Fortschritt: HTTP %d (Dok %d, progress=%s, status=%s)",
                        resp.getStatus(), docId, progress, status);
            }
        } catch (Exception e) {
            LOG.warnf("Dokument-Fortschritt konnte nicht gesetzt werden (Dok %d): %s", docId, e.getMessage());
        }
    }

    /**
     * Erkennt externe Referenzen im Dokumenttext, lädt sie sicher herunter,
     * legt sie als eigene dc_project_documents-Zeilen an (is_reference=1) und
     * sammelt ihren Original-Text für die spätere „Summen"-Auswertung.
     */
    private void processReferences(OrdsDocument doc, String originalText, long projectId,
                                   Map<Long, List<String>> referenceTextsByDoc) {
        List<String> urls = evaluationService.extractReferenceUrls(originalText, projectId);
        if (urls.isEmpty()) return;

        LOG.infof("Dokument %d: %d Referenz-URL(s) vom Modell erkannt", doc.getId(), urls.size());
        int processed = 0;

        for (String url : urls) {
            if (processed >= maxReferencesPerDocument) {
                LOG.infof("Dokument %d: Referenz-Limit (%d) erreicht – Rest ignoriert",
                        doc.getId(), maxReferencesPerDocument);
                break;
            }

            var fetched = referenceDownloadService.fetch(url, projectId);
            if (fetched.isEmpty()) continue;

            ReferenceDownloadService.DownloadedReference ref = fetched.get();
            try {
                // 1. Referenz als neue Dokument-Zeile (mit BLOB) anlegen
                String base64 = Base64.getEncoder().encodeToString(ref.bytes());
                Response insResp = ordsClient.insertReference(doc.getId(),
                        new ReferenceCreateRequest(ref.sourceUrl(), ref.filename(),
                                ref.mimeType(), base64));
                String body = insResp.readEntity(String.class);
                if (insResp.getStatus() != 201) {
                    LOG.warnf("Referenz-Insert fehlgeschlagen (HTTP %d): %s", insResp.getStatus(), body);
                    continue;
                }
                long refId = extractId(body);

                // 2. Texte der Referenz speichern (Original + Übersetzung) – für separate Ansicht
                String refTranslated = evaluationService.translate(ref.text(), projectId);
                ordsClient.updateTexts(refId, new TextsRequest(ref.text(), refTranslated));
                pushDocumentProgress(refId, 100, "COMPLETED");

                // 3. Original-Text für die „Summen"-Auswertung des Hauptdokuments merken
                referenceTextsByDoc.computeIfAbsent(doc.getId(), k -> new ArrayList<>())
                        .add("<!-- Referenz: " + ref.sourceUrl() + " -->\n" + ref.text());

                processed++;
                LOG.infof("Dokument %d: Referenz gespeichert (id=%d, %s)",
                        doc.getId(), refId, ref.sourceUrl());
            } catch (Exception e) {
                LOG.warnf("Referenz '%s' konnte nicht gespeichert werden: %s", url, e.getMessage());
            }
        }
    }

    /** Liest das Feld "id" aus einer ORDS-Insert-Antwort. */
    private long extractId(String responseBody) throws Exception {
        Map<?, ?> 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: " + responseBody);
    }

    /**
     * Fügt Haupttext und Referenztexte zu einem Bewertungstext zusammen (Option D).
     * Die Referenzen werden klar abgegrenzt angehängt.
     */
    private String buildCombinedText(String mainText, List<String> referenceTexts) {
        if (referenceTexts == null || referenceTexts.isEmpty()) {
            return mainText != null ? mainText : "";
        }
        StringBuilder sb = new StringBuilder(mainText != null ? mainText : "");
        for (String refText : referenceTexts) {
            sb.append("\n\n--- Referenzdokument ---\n\n").append(refText);
        }
        return sb.toString();
    }

    /** Teilt eine Liste in Untergruppen der gewünschten Größe auf. */
    private static <T> List<List<T>> partition(List<T> list, int size) {
        List<List<T>> parts = new ArrayList<>();
        for (int i = 0; i < list.size(); i += size) {
            parts.add(list.subList(i, Math.min(i + size, list.size())));
        }
        return parts;
    }

    /** Fallback: wertet jede Frage einzeln aus, gibt Map question_id → result zurück. */
    private Map<Long, EvaluationResult> evaluateIndividually(List<OrdsQuestion> questions,
                                                              String documentText,
                                                              long projectId) {
        Map<Long, EvaluationResult> map = new HashMap<>();
        for (OrdsQuestion q : questions) {
            try {
                map.put(q.getQuestionId(),
                        evaluationService.evaluate(q, documentText, projectId));
            } catch (Exception e) {
                LOG.errorf(e, "Einzelauswertung fehlgeschlagen: Frage %d", q.getQuestionId());
            }
        }
        return map;
    }

    private ResultRequest saveResult(long projectId, long docId,
                                     OrdsQuestion question, EvaluationResult result) {
        try {
            int deviation = 0, warning = 0;
            if (result.getScore() != null && question.getThreshold() != null
                    && result.getScore() < question.getThreshold()) {
                if ("UNKLAR".equalsIgnoreCase(result.getResultType())) {
                    // Teilweise Erfüllung (falscher Paragraph, gleiches Ziel, o.ä.) → immer Hinweis
                    warning = 1;
                } else if ("ABWEICHUNG".equalsIgnoreCase(question.getResultHandling())) {
                    deviation = 1;
                } else if ("HINWEIS".equalsIgnoreCase(question.getResultHandling())) {
                    warning = 1;
                }
            }
            ResultRequest rr = new ResultRequest(
                question.getQuestionId(), docId,
                result.getAnswer(), result.getScore(), result.getResultType(),
                deviation, warning, result.getTheComment()
            );
            Response saveResp = ordsClient.saveResult(projectId, rr);
            if (saveResp.getStatus() != 201) {
                LOG.warnf("Ergebnis speichern: HTTP %d (Frage %d, Dok %d)",
                    saveResp.getStatus(), question.getQuestionId(), docId);
            }
            return rr;
        } catch (Exception e) {
            LOG.errorf(e, "Ergebnis-Speichern fehlgeschlagen: Frage %d / Dokument %d",
                question.getQuestionId(), docId);
            return null;
        }
    }

    /**
     * Berechnet die geschätzte Fertigstellung auf Basis der bisher verstrichenen Zeit.
     * ETA = jetzt + (verbleibende Schritte × Ø Dauer pro Schritt)
     */
    private String calcEta(Instant startedAt, int stepsCompleted, int totalSteps) {
        if (stepsCompleted <= 0) return null;
        long elapsedMs  = Duration.between(startedAt, Instant.now()).toMillis();
        long avgMs      = elapsedMs / stepsCompleted;
        int  remaining  = totalSteps - stepsCompleted;
        if (remaining <= 0) return null;
        Instant eta = Instant.now().plusMillis(avgMs * remaining).truncatedTo(ChronoUnit.SECONDS);
        return ISO_FMT.format(eta.atZone(ZoneId.systemDefault()));
    }
}
