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.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;

    // =========================================================================
    // 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);
                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 löschen (ermöglicht Re-Processing)
            Response delResp = ordsClient.deleteResults(projectId);
            LOG.debugf("Alte Ergebnisse gelöscht: HTTP %d", delResp.getStatus());

            // Texte sammeln (in-memory: vermeidet Re-Fetch aus ORDS)
            Map<Long, String> originalTexts = new HashMap<>();

            // ─── PHASE A: OCR + Übersetzung (wird übersprungen wenn Text bereits vorhanden) ──
            for (OrdsDocument doc : documents) {
                // Vor dem Schritt: Status auf "OCR für dieses Dokument" setzen
                pushProgress(projectId, stepHolder[0], totalSteps, "OCR",
                             doc.getId(), null, startedAt);

                String originalText = "";

                if (Integer.valueOf(1).equals(doc.getHasOriginalText())) {
                    // Resume: Text aus vorherigem Lauf wiederverwenden
                    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() : "";
                    } 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());
                        } 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());

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

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

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

                // Nach dem Schritt: Fortschritt hochzählen + ETA aktualisieren
                stepHolder[0]++;
                pushProgress(projectId, stepHolder[0], totalSteps, "OCR",
                             doc.getId(), null, startedAt);
            }

            // ─── PHASE B: Fragen auswerten ────────────────────────────────
            for (OrdsDocument doc : documents) {
                String originalText = originalTexts.getOrDefault(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);

                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;
                        saveResult(projectId, doc.getId(), question, result);
                    }

                    stepHolder[0] += batch.size();
                    pushProgress(projectId, stepHolder[0], totalSteps, "QUESTIONS",
                                 doc.getId(), batch.get(batch.size() - 1).getQuestionId(), startedAt);
                }
            }

            // 7. Abschließen
            ordsClient.completeProject(projectId);
            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());
        }
    }

    /** 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 void 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 ("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);
            }
        } catch (Exception e) {
            LOG.errorf(e, "Ergebnis-Speichern fehlgeschlagen: Frage %d / Dokument %d",
                question.getQuestionId(), docId);
        }
    }

    /**
     * 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()));
    }
}
