package de.frigosped.dc.service;

import de.frigosped.dc.client.MistralOcrClient;
import de.frigosped.dc.model.MistralOcrRequest;
import de.frigosped.dc.model.MistralOcrResponse;
import dev.langchain4j.data.message.ImageContent;
import dev.langchain4j.data.message.TextContent;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.ChatLanguageModel;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.poi.xwpf.usermodel.*;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;
import org.odftoolkit.odfdom.doc.OdfTextDocument;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;

/**
 * Konvertiert hochgeladene Dokumente in Markdown-Text.
 *
 * Unterstützte Formate:
 *   - PDF  → KI-OCR (jede Seite als Bild an quen3.5:9b)
 *   - DOCX → Apache POI → Markdown
 *   - ODT  → ODF Toolkit → Markdown
 *   - TXT  → direkt als Markdown
 */
@ApplicationScoped
public class DocumentProcessingService {

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

    /** 200 DPI: gutes Gleichgewicht zwischen Qualität und Dateigröße für OCR */
    private static final float OCR_DPI = 200f;

    /** Mindest-Zeichen pro Seite – darunter gilt das PDF als gescannt */
    private static final int MIN_CHARS_PER_PAGE = 50;

    @Inject
    @Named("ocr")
    ChatLanguageModel ocrModel;

    @Inject
    @RestClient
    MistralOcrClient mistralOcrClient;

    @Inject
    AiCostTrackerService costTracker;

    @ConfigProperty(name = "dc.ai.provider", defaultValue = "ollama")
    String aiProvider;

    @ConfigProperty(name = "dc.ai.mistral.api-key", defaultValue = "")
    String mistralApiKey;

    // =========================================================================
    // Öffentliche API
    // =========================================================================

    /**
     * Extrahiert den Textinhalt eines Dokuments als Markdown.
     *
     * @param fileBytes  rohe Datei-Bytes (aus BLOB)
     * @param mimeType   MIME-Typ des Dokuments
     * @param filename   Dateiname (Fallback für MIME-Typ-Erkennung)
     * @return Markdown-String
     */
    public String extractText(byte[] fileBytes, String mimeType, String filename) {
        return extractText(fileBytes, mimeType, filename, null);
    }

    public String extractText(byte[] fileBytes, String mimeType, String filename, Long projectId) {
        String effectiveMime = resolveMimeType(mimeType, filename);
        LOG.debugf("Extrahiere Text: %s (%s)", filename, effectiveMime);

        try {
            return switch (effectiveMime) {
                case "application/pdf"
                     -> extractFromPdf(fileBytes, projectId);
                case "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                     "application/msword"
                     -> extractFromDocx(fileBytes);
                case "application/vnd.oasis.opendocument.text"
                     -> extractFromOdt(fileBytes);
                case "text/plain", "text/markdown"
                     -> new String(fileBytes, java.nio.charset.StandardCharsets.UTF_8);
                default -> {
                    LOG.warnf("Unbekannter MIME-Typ '%s' – versuche als Text", effectiveMime);
                    yield new String(fileBytes, java.nio.charset.StandardCharsets.UTF_8);
                }
            };
        } catch (Exception e) {
            LOG.errorf(e, "Fehler bei Textextraktion (%s)", filename);
            throw new RuntimeException("Textextraktion fehlgeschlagen: " + e.getMessage(), e);
        }
    }

    // =========================================================================
    // PDF: Seiten rendern + KI-OCR
    // =========================================================================

    private String extractFromPdf(byte[] fileBytes, Long projectId) throws Exception {
        try (PDDocument pdf = Loader.loadPDF(fileBytes)) {
            int pageCount = pdf.getNumberOfPages();
            LOG.debugf("PDF hat %d Seite(n)", pageCount);

            String extracted = new PDFTextStripper().getText(pdf).trim();
            if (extracted.length() >= pageCount * MIN_CHARS_PER_PAGE) {
                LOG.debugf("PDF-Textextraktion: %d Zeichen", extracted.length());
                return extracted;
            }

            LOG.debugf("PDF hat wenig eingebetteten Text (%d Zeichen) – verwende Vision-OCR", extracted.length());
            if ("mistral".equalsIgnoreCase(aiProvider)) {
                return mistralOcr(fileBytes);
            }
            PDFRenderer renderer = new PDFRenderer(pdf);
            StringBuilder result = new StringBuilder();
            for (int i = 0; i < pageCount; i++) {
                LOG.debugf("OCR Seite %d/%d", i + 1, pageCount);
                BufferedImage image = renderer.renderImageWithDPI(i, OCR_DPI, ImageType.RGB);
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ImageIO.write(image, "PNG", baos);
                String base64Image = Base64.getEncoder().encodeToString(baos.toByteArray());
                result.append(ocrPage(base64Image, i + 1, pageCount, projectId)).append("\n\n");
            }
            return result.toString().trim();
        }
    }

    private String ocrPage(String base64Image, int pageNum, int totalPages, Long projectId) {
        UserMessage message = UserMessage.from(
            ImageContent.from(base64Image, "image/png"),
            TextContent.from(
                "Du bist ein OCR-Spezialist. " +
                "Extrahiere den gesamten Text aus diesem Dokument-Bild (Seite " + pageNum
                + " von " + totalPages + ").\n\n" +
                "Regeln:\n" +
                "- Formatiere den Text als Markdown\n" +
                "- Überschriften als # / ## / ###\n" +
                "- Tabellen als Markdown-Tabellen\n" +
                "- Listen als - oder 1. 2. 3.\n" +
                "- Behalte den Originaltext exakt (keine Korrekturen, keine Zusammenfassungen)\n" +
                "- Antworte NUR mit dem extrahierten Markdown, ohne Präambel"
            )
        );

        dev.langchain4j.model.output.Response<dev.langchain4j.data.message.AiMessage> response =
                ocrModel.generate(List.of(message));
        costTracker.track("ocr", "OCR", projectId, null, response.tokenUsage());
        return response.content().text();
    }

    private String mistralOcr(byte[] fileBytes) {
        LOG.debug("Mistral OCR API – sende PDF direkt...");
        String base64 = Base64.getEncoder().encodeToString(fileBytes);
        String dataUrl = "data:application/pdf;base64," + base64;

        MistralOcrRequest request = new MistralOcrRequest(
                "mistral-ocr-latest",
                new MistralOcrRequest.Document("document_url", dataUrl)
        );

        MistralOcrResponse response = mistralOcrClient.ocr("Bearer " + mistralApiKey, request);

        StringBuilder result = new StringBuilder();
        for (MistralOcrResponse.Page page : response.getPages().stream()
                .sorted(java.util.Comparator.comparingInt(MistralOcrResponse.Page::getIndex))
                .toList()) {
            if (page.getMarkdown() != null && !page.getMarkdown().isBlank()) {
                result.append(page.getMarkdown()).append("\n\n");
            }
        }
        LOG.debugf("Mistral OCR abgeschlossen: %d Seite(n), %d Zeichen",
                response.getPages().size(), result.length());
        return result.toString().trim();
    }

    // =========================================================================
    // DOCX → Markdown (Apache POI)
    // =========================================================================

    private String extractFromDocx(byte[] fileBytes) throws Exception {
        LOG.debug("Konvertiere DOCX → Markdown...");
        StringBuilder md = new StringBuilder();

        try (XWPFDocument doc = new XWPFDocument(new ByteArrayInputStream(fileBytes))) {
            for (IBodyElement element : doc.getBodyElements()) {
                if (element instanceof XWPFParagraph para) {
                    md.append(paragraphToMarkdown(para)).append("\n");
                } else if (element instanceof XWPFTable table) {
                    md.append(tableToMarkdown(table)).append("\n");
                }
            }
        }

        return md.toString().trim();
    }

    private String paragraphToMarkdown(XWPFParagraph para) {
        String text = para.getText();
        if (text == null || text.isBlank()) return "";

        String style = para.getStyle();
        if (style == null) return text;

        // Überschriften
        if (style.equalsIgnoreCase("Heading1") || style.equalsIgnoreCase("berschrift1"))
            return "# " + text;
        if (style.equalsIgnoreCase("Heading2") || style.equalsIgnoreCase("berschrift2"))
            return "## " + text;
        if (style.equalsIgnoreCase("Heading3") || style.equalsIgnoreCase("berschrift3"))
            return "### " + text;
        if (style.equalsIgnoreCase("Heading4") || style.equalsIgnoreCase("berschrift4"))
            return "#### " + text;

        // Nummerierte Liste
        if (para.getNumIlvl() != null) {
            return "- " + text;
        }

        return text;
    }

    private String tableToMarkdown(XWPFTable table) {
        StringBuilder sb = new StringBuilder();
        List<XWPFTableRow> rows = table.getRows();
        if (rows.isEmpty()) return "";

        // Header-Zeile
        XWPFTableRow header = rows.get(0);
        sb.append("| ");
        header.getTableCells().forEach(c -> sb.append(c.getText()).append(" | "));
        sb.append("\n|");
        header.getTableCells().forEach(c -> sb.append("---|"));
        sb.append("\n");

        // Daten-Zeilen
        for (int i = 1; i < rows.size(); i++) {
            sb.append("| ");
            rows.get(i).getTableCells().forEach(c -> sb.append(c.getText()).append(" | "));
            sb.append("\n");
        }

        return sb.toString();
    }

    // =========================================================================
    // ODT → Markdown (ODF Toolkit)
    // =========================================================================

    private String extractFromOdt(byte[] fileBytes) throws Exception {
        LOG.debug("Konvertiere ODT → Markdown...");
        StringBuilder md = new StringBuilder();

        try (OdfTextDocument odfDoc = OdfTextDocument.loadDocument(
                new ByteArrayInputStream(fileBytes))) {

            NodeList paragraphs = odfDoc.getContentDom()
                    .getElementsByTagName("text:p");

            for (int i = 0; i < paragraphs.getLength(); i++) {
                Node node = paragraphs.item(i);
                String text = node.getTextContent();
                if (text != null && !text.isBlank()) {
                    // Stil-Attribut auslesen
                    Node styleAttr = node.getAttributes()
                            .getNamedItem("text:style-name");
                    String style = styleAttr != null ? styleAttr.getNodeValue() : "";

                    if (style.startsWith("Heading_20_1") || style.startsWith("Heading1"))
                        md.append("# ").append(text).append("\n");
                    else if (style.startsWith("Heading_20_2") || style.startsWith("Heading2"))
                        md.append("## ").append(text).append("\n");
                    else if (style.startsWith("Heading_20_3") || style.startsWith("Heading3"))
                        md.append("### ").append(text).append("\n");
                    else
                        md.append(text).append("\n");
                }
            }
        }

        return md.toString().trim();
    }

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

    /**
     * Leitet MIME-Typ aus Dateiendung ab, wenn der gespeicherte MIME-Typ
     * fehlt oder generic ist.
     */
    private String resolveMimeType(String mimeType, String filename) {
        if (mimeType != null && !mimeType.isBlank()
                && !mimeType.equals("application/octet-stream")) {
            return mimeType;
        }
        if (filename == null) return "application/octet-stream";
        String lower = filename.toLowerCase();
        if (lower.endsWith(".pdf"))  return "application/pdf";
        if (lower.endsWith(".docx")) return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
        if (lower.endsWith(".doc"))  return "application/msword";
        if (lower.endsWith(".odt"))  return "application/vnd.oasis.opendocument.text";
        if (lower.endsWith(".txt"))  return "text/plain";
        if (lower.endsWith(".md"))   return "text/markdown";
        return "application/octet-stream";
    }
}
