"""
File Processor Service - Dokumentenverarbeitung für BreakPilot.

Shared Service für:
- OCR (Optical Character Recognition) für Handschrift und gedruckten Text
- PDF-Parsing und Textextraktion
- Bildverarbeitung und -optimierung
- DOCX/DOC Textextraktion

Verwendet:
- PaddleOCR für deutsche Handschrift
- PyMuPDF für PDF-Verarbeitung
- python-docx für DOCX-Dateien
- OpenCV für Bildvorverarbeitung
"""

import logging
import os
import io
import base64
from pathlib import Path
from typing import Optional, List, Dict, Any, Tuple, Union
from dataclasses import dataclass
from enum import Enum

import cv2
import numpy as np
from PIL import Image

logger = logging.getLogger(__name__)


class FileType(str, Enum):
    """Unterstützte Dateitypen."""
    PDF = "pdf"
    IMAGE = "image"
    DOCX = "docx"
    DOC = "doc"
    TXT = "txt"
    UNKNOWN = "unknown"


class ProcessingMode(str, Enum):
    """Verarbeitungsmodi."""
    OCR_HANDWRITING = "ocr_handwriting"  # Handschrifterkennung
    OCR_PRINTED = "ocr_printed"          # Gedruckter Text
    TEXT_EXTRACT = "text_extract"        # Textextraktion (PDF/DOCX)
    MIXED = "mixed"                       # Kombiniert OCR + Textextraktion


@dataclass
class ProcessedRegion:
    """Ein erkannter Textbereich."""
    text: str
    confidence: float
    bbox: Tuple[int, int, int, int]  # x1, y1, x2, y2
    page: int = 1


@dataclass
class ProcessingResult:
    """Ergebnis der Dokumentenverarbeitung."""
    text: str
    confidence: float
    regions: List[ProcessedRegion]
    page_count: int
    file_type: FileType
    processing_mode: ProcessingMode
    metadata: Dict[str, Any]


class FileProcessor:
    """
    Zentrale Dokumentenverarbeitung für BreakPilot.

    Unterstützt:
    - Handschrifterkennung (OCR) für Klausuren
    - Textextraktion aus PDFs
    - DOCX/DOC Verarbeitung
    - Bildvorverarbeitung für bessere OCR-Ergebnisse
    """

    def __init__(self, ocr_lang: str = "de", use_gpu: bool = False):
        """
        Initialisiert den File Processor.

        Args:
            ocr_lang: Sprache für OCR (default: "de" für Deutsch)
            use_gpu: GPU für OCR nutzen (beschleunigt Verarbeitung)
        """
        self.ocr_lang = ocr_lang
        self.use_gpu = use_gpu
        self._ocr_engine = None

        logger.info(f"FileProcessor initialized (lang={ocr_lang}, gpu={use_gpu})")

    @property
    def ocr_engine(self):
        """Lazy-Loading des OCR-Engines."""
        if self._ocr_engine is None:
            self._ocr_engine = self._init_ocr_engine()
        return self._ocr_engine

    def _init_ocr_engine(self):
        """Initialisiert PaddleOCR oder Fallback."""
        try:
            from paddleocr import PaddleOCR
            return PaddleOCR(
                use_angle_cls=True,
                lang='german',  # Deutsch
                use_gpu=self.use_gpu,
                show_log=False
            )
        except ImportError:
            logger.warning("PaddleOCR nicht installiert - verwende Fallback")
            return None

    def detect_file_type(self, file_path: str = None, file_bytes: bytes = None) -> FileType:
        """
        Erkennt den Dateityp.

        Args:
            file_path: Pfad zur Datei
            file_bytes: Dateiinhalt als Bytes

        Returns:
            FileType enum
        """
        if file_path:
            ext = Path(file_path).suffix.lower()
            if ext == ".pdf":
                return FileType.PDF
            elif ext in [".jpg", ".jpeg", ".png", ".bmp", ".tiff", ".gif"]:
                return FileType.IMAGE
            elif ext == ".docx":
                return FileType.DOCX
            elif ext == ".doc":
                return FileType.DOC
            elif ext == ".txt":
                return FileType.TXT

        if file_bytes:
            # Magic number detection
            if file_bytes[:4] == b'%PDF':
                return FileType.PDF
            elif file_bytes[:8] == b'\x89PNG\r\n\x1a\n':
                return FileType.IMAGE
            elif file_bytes[:2] in [b'\xff\xd8', b'BM']:  # JPEG, BMP
                return FileType.IMAGE
            elif file_bytes[:4] == b'PK\x03\x04':  # ZIP (DOCX)
                return FileType.DOCX

        return FileType.UNKNOWN

    def process(
        self,
        file_path: str = None,
        file_bytes: bytes = None,
        mode: ProcessingMode = ProcessingMode.MIXED
    ) -> ProcessingResult:
        """
        Verarbeitet ein Dokument.

        Args:
            file_path: Pfad zur Datei
            file_bytes: Dateiinhalt als Bytes
            mode: Verarbeitungsmodus

        Returns:
            ProcessingResult mit extrahiertem Text und Metadaten
        """
        if not file_path and not file_bytes:
            raise ValueError("Entweder file_path oder file_bytes muss angegeben werden")

        file_type = self.detect_file_type(file_path, file_bytes)
        logger.info(f"Processing file of type: {file_type}")

        if file_type == FileType.PDF:
            return self._process_pdf(file_path, file_bytes, mode)
        elif file_type == FileType.IMAGE:
            return self._process_image(file_path, file_bytes, mode)
        elif file_type == FileType.DOCX:
            return self._process_docx(file_path, file_bytes)
        elif file_type == FileType.TXT:
            return self._process_txt(file_path, file_bytes)
        else:
            raise ValueError(f"Nicht unterstützter Dateityp: {file_type}")

    def _process_pdf(
        self,
        file_path: str = None,
        file_bytes: bytes = None,
        mode: ProcessingMode = ProcessingMode.MIXED
    ) -> ProcessingResult:
        """Verarbeitet PDF-Dateien."""
        try:
            import fitz  # PyMuPDF
        except ImportError:
            logger.warning("PyMuPDF nicht installiert - versuche Fallback")
            # Fallback: PDF als Bild behandeln
            return self._process_image(file_path, file_bytes, mode)

        if file_bytes:
            doc = fitz.open(stream=file_bytes, filetype="pdf")
        else:
            doc = fitz.open(file_path)

        all_text = []
        all_regions = []
        total_confidence = 0.0
        region_count = 0

        for page_num, page in enumerate(doc, start=1):
            # Erst versuchen Text direkt zu extrahieren
            page_text = page.get_text()

            if page_text.strip() and mode != ProcessingMode.OCR_HANDWRITING:
                # PDF enthält Text (nicht nur Bilder)
                all_text.append(page_text)
                all_regions.append(ProcessedRegion(
                    text=page_text,
                    confidence=1.0,
                    bbox=(0, 0, int(page.rect.width), int(page.rect.height)),
                    page=page_num
                ))
                total_confidence += 1.0
                region_count += 1
            else:
                # Seite als Bild rendern und OCR anwenden
                pix = page.get_pixmap(matrix=fitz.Matrix(2, 2))  # 2x Auflösung
                img_bytes = pix.tobytes("png")
                img = Image.open(io.BytesIO(img_bytes))

                ocr_result = self._ocr_image(img)
                all_text.append(ocr_result["text"])

                for region in ocr_result["regions"]:
                    region.page = page_num
                    all_regions.append(region)
                    total_confidence += region.confidence
                    region_count += 1

        doc.close()

        avg_confidence = total_confidence / region_count if region_count > 0 else 0.0

        return ProcessingResult(
            text="\n\n".join(all_text),
            confidence=avg_confidence,
            regions=all_regions,
            page_count=len(doc) if hasattr(doc, '__len__') else 1,
            file_type=FileType.PDF,
            processing_mode=mode,
            metadata={"source": file_path or "bytes"}
        )

    def _process_image(
        self,
        file_path: str = None,
        file_bytes: bytes = None,
        mode: ProcessingMode = ProcessingMode.MIXED
    ) -> ProcessingResult:
        """Verarbeitet Bilddateien."""
        if file_bytes:
            img = Image.open(io.BytesIO(file_bytes))
        else:
            img = Image.open(file_path)

        # Bildvorverarbeitung
        processed_img = self._preprocess_image(img)

        # OCR
        ocr_result = self._ocr_image(processed_img)

        return ProcessingResult(
            text=ocr_result["text"],
            confidence=ocr_result["confidence"],
            regions=ocr_result["regions"],
            page_count=1,
            file_type=FileType.IMAGE,
            processing_mode=mode,
            metadata={
                "source": file_path or "bytes",
                "image_size": img.size
            }
        )

    def _process_docx(
        self,
        file_path: str = None,
        file_bytes: bytes = None
    ) -> ProcessingResult:
        """Verarbeitet DOCX-Dateien."""
        try:
            from docx import Document
        except ImportError:
            raise ImportError("python-docx ist nicht installiert")

        if file_bytes:
            doc = Document(io.BytesIO(file_bytes))
        else:
            doc = Document(file_path)

        paragraphs = []
        for para in doc.paragraphs:
            if para.text.strip():
                paragraphs.append(para.text)

        # Auch Tabellen extrahieren
        for table in doc.tables:
            for row in table.rows:
                row_text = " | ".join(cell.text for cell in row.cells)
                if row_text.strip():
                    paragraphs.append(row_text)

        text = "\n\n".join(paragraphs)

        return ProcessingResult(
            text=text,
            confidence=1.0,  # Direkte Textextraktion
            regions=[ProcessedRegion(
                text=text,
                confidence=1.0,
                bbox=(0, 0, 0, 0),
                page=1
            )],
            page_count=1,
            file_type=FileType.DOCX,
            processing_mode=ProcessingMode.TEXT_EXTRACT,
            metadata={"source": file_path or "bytes"}
        )

    def _process_txt(
        self,
        file_path: str = None,
        file_bytes: bytes = None
    ) -> ProcessingResult:
        """Verarbeitet Textdateien."""
        if file_bytes:
            text = file_bytes.decode('utf-8', errors='ignore')
        else:
            with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
                text = f.read()

        return ProcessingResult(
            text=text,
            confidence=1.0,
            regions=[ProcessedRegion(
                text=text,
                confidence=1.0,
                bbox=(0, 0, 0, 0),
                page=1
            )],
            page_count=1,
            file_type=FileType.TXT,
            processing_mode=ProcessingMode.TEXT_EXTRACT,
            metadata={"source": file_path or "bytes"}
        )

    def _preprocess_image(self, img: Image.Image) -> Image.Image:
        """
        Vorverarbeitung des Bildes für bessere OCR-Ergebnisse.

        - Konvertierung zu Graustufen
        - Kontrastverstärkung
        - Rauschunterdrückung
        - Binarisierung
        """
        # PIL zu OpenCV
        cv_img = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)

        # Zu Graustufen konvertieren
        gray = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY)

        # Rauschunterdrückung
        denoised = cv2.fastNlMeansDenoising(gray, None, 10, 7, 21)

        # Kontrastverstärkung (CLAHE)
        clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
        enhanced = clahe.apply(denoised)

        # Adaptive Binarisierung
        binary = cv2.adaptiveThreshold(
            enhanced,
            255,
            cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
            cv2.THRESH_BINARY,
            11,
            2
        )

        # Zurück zu PIL
        return Image.fromarray(binary)

    def _ocr_image(self, img: Image.Image) -> Dict[str, Any]:
        """
        Führt OCR auf einem Bild aus.

        Returns:
            Dict mit text, confidence und regions
        """
        if self.ocr_engine is None:
            # Fallback wenn kein OCR-Engine verfügbar
            return {
                "text": "[OCR nicht verfügbar - bitte PaddleOCR installieren]",
                "confidence": 0.0,
                "regions": []
            }

        # PIL zu numpy array
        img_array = np.array(img)

        # Wenn Graustufen, zu RGB konvertieren (PaddleOCR erwartet RGB)
        if len(img_array.shape) == 2:
            img_array = cv2.cvtColor(img_array, cv2.COLOR_GRAY2RGB)

        # OCR ausführen
        result = self.ocr_engine.ocr(img_array, cls=True)

        if not result or not result[0]:
            return {"text": "", "confidence": 0.0, "regions": []}

        all_text = []
        all_regions = []
        total_confidence = 0.0

        for line in result[0]:
            bbox_points = line[0]  # [[x1,y1], [x2,y2], [x3,y3], [x4,y4]]
            text, confidence = line[1]

            # Bounding Box zu x1, y1, x2, y2 konvertieren
            x_coords = [p[0] for p in bbox_points]
            y_coords = [p[1] for p in bbox_points]
            bbox = (
                int(min(x_coords)),
                int(min(y_coords)),
                int(max(x_coords)),
                int(max(y_coords))
            )

            all_text.append(text)
            all_regions.append(ProcessedRegion(
                text=text,
                confidence=confidence,
                bbox=bbox
            ))
            total_confidence += confidence

        avg_confidence = total_confidence / len(all_regions) if all_regions else 0.0

        return {
            "text": "\n".join(all_text),
            "confidence": avg_confidence,
            "regions": all_regions
        }

    def extract_handwriting_regions(
        self,
        img: Image.Image,
        min_area: int = 500
    ) -> List[Dict[str, Any]]:
        """
        Erkennt und extrahiert handschriftliche Bereiche aus einem Bild.

        Nützlich für Klausuren mit gedruckten Fragen und handschriftlichen Antworten.

        Args:
            img: Eingabebild
            min_area: Minimale Fläche für erkannte Regionen

        Returns:
            Liste von Regionen mit Koordinaten und erkanntem Text
        """
        # Bildvorverarbeitung
        cv_img = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
        gray = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY)

        # Kanten erkennen
        edges = cv2.Canny(gray, 50, 150)

        # Morphologische Operationen zum Verbinden
        kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 5))
        dilated = cv2.dilate(edges, kernel, iterations=2)

        # Konturen finden
        contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

        regions = []
        for contour in contours:
            area = cv2.contourArea(contour)
            if area < min_area:
                continue

            x, y, w, h = cv2.boundingRect(contour)

            # Region ausschneiden
            region_img = img.crop((x, y, x + w, y + h))

            # OCR auf Region anwenden
            ocr_result = self._ocr_image(region_img)

            regions.append({
                "bbox": (x, y, x + w, y + h),
                "area": area,
                "text": ocr_result["text"],
                "confidence": ocr_result["confidence"]
            })

        # Nach Y-Position sortieren (oben nach unten)
        regions.sort(key=lambda r: r["bbox"][1])

        return regions


# Singleton-Instanz
_file_processor: Optional[FileProcessor] = None


def get_file_processor() -> FileProcessor:
    """Gibt Singleton-Instanz des File Processors zurück."""
    global _file_processor
    if _file_processor is None:
        _file_processor = FileProcessor()
    return _file_processor


# Convenience functions
def process_file(
    file_path: str = None,
    file_bytes: bytes = None,
    mode: ProcessingMode = ProcessingMode.MIXED
) -> ProcessingResult:
    """
    Convenience function zum Verarbeiten einer Datei.

    Args:
        file_path: Pfad zur Datei
        file_bytes: Dateiinhalt als Bytes
        mode: Verarbeitungsmodus

    Returns:
        ProcessingResult
    """
    processor = get_file_processor()
    return processor.process(file_path, file_bytes, mode)


def extract_text_from_pdf(file_path: str = None, file_bytes: bytes = None) -> str:
    """Extrahiert Text aus einer PDF-Datei."""
    result = process_file(file_path, file_bytes, ProcessingMode.TEXT_EXTRACT)
    return result.text


def ocr_image(file_path: str = None, file_bytes: bytes = None) -> str:
    """Führt OCR auf einem Bild aus."""
    result = process_file(file_path, file_bytes, ProcessingMode.OCR_PRINTED)
    return result.text


def ocr_handwriting(file_path: str = None, file_bytes: bytes = None) -> str:
    """Führt Handschrift-OCR auf einem Bild aus."""
    result = process_file(file_path, file_bytes, ProcessingMode.OCR_HANDWRITING)
    return result.text
