import logging
import shutil

from PySide6.QtCore import Qt, QEvent
from PySide6.QtGui import QAction
from PySide6.QtWidgets import (
    QMainWindow,
    QApplication,
    QStyleFactory,
    QTreeWidgetItem,
    QMessageBox,
    QMenu,
)

from ui.MainWinddow_ui import Ui_MainWindow
from ui.AppSettings import AppSettingsDlg
from ui.PdfProject import PdfProjectDlg
from ui.mixins import (
    TreeManagerMixin,
    PdfViewerMixin,
    WorkerPoolMixin,
    DatabaseMixin,
    DragDropMixin,
    HashCalculationMixin,
    TransformationMixin,
)
from conf import app_settings, Project, ProjectData, TreeNode, XslFile
from icons import icon, IconRefreshMixin
from pathlib import Path


logger = logging.getLogger(__name__)


class MainWindow(
    IconRefreshMixin,
    QMainWindow,
    TreeManagerMixin,
    PdfViewerMixin,
    WorkerPoolMixin,
    DatabaseMixin,
    DragDropMixin,
    HashCalculationMixin,
    TransformationMixin,
):
    def __init__(self, parent=None):
        """
        Konstruktor für die MainWindow-Klasse.
        Verwendet PySide6.QtPdf für optimale Performance.

        Args:
            parent: Übergeordnetes Widget, falls vorhanden
        """
        super().__init__(parent)

        # UI einrichten
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)

        # Dict zum Speichern der Beziehung zwischen Thumbnails und Seitennummern
        self.thumbnail_to_page = {}

        # PDF-Dokumente für späteres On-Demand-Rendering speichern
        self.pdf_documents = {}  # {pdf_filename: {'diff': QPdfDocument, 'ref': QPdfDocument, 'new': QPdfDocument}}

        # Aktueller Zoom-Faktor
        self.current_zoom = 100  # 100%

        # Aktuell angezeigte Seite
        self.current_page = 0
        self.current_pdf = None

        # Aktuelle Diff-PDF-Informationen (für Accept Changes)
        self.current_diff_xml_path = None
        self.current_diff_xsl_id = None

        # Pfade zu aktuellen Ref- und New-PDFs (für System-Viewer)
        self.current_ref_pdf_path = None
        self.current_new_pdf_path = None

        # Cache für die aktuell gerenderten Pixmaps (Performance-Optimierung)
        self.current_rendered_pixmaps = None

        # Label für die Vollansicht (nur ein einziges Label)
        self.fullsize_label = None

        # Variablen für Drag-to-Scroll (Anti-Jitter für 4K/DPI-Skalierung)
        self.is_dragging = False
        self.last_drag_position = None
        self.drag_threshold = 3  # Mindestbewegung in Pixeln vor dem Scrollen
        self.scroll_sensitivity = 0.7  # Reduzierte Empfindlichkeit für sanfteres Scrollen

        # Das aktuelle Projekt (Project) aus app_settings
        self.project = None

        # Das aktuelle ProjectData
        self.pdf_project = None

        # Hash-Berechnungs-Thread
        self.hash_calculator_thread = None

        # Transformations-Thread
        self.transformation_thread = None

        # Batch-Processing-Thread für XML-Dateien
        self.batch_processing_thread = None

        # Progressbar für Batch-Verarbeitung in Statusbar
        self.batch_progress_bar = None

        # Progressbar für Transformationen in Statusbar
        self.transformation_progress_bar = None

        # Gespeicherte Worker-Pool-Metriken (werden vor Shutdown gecacht)
        self.last_saxon_metrics = None
        self.last_fop_metrics = None

        # Mapping: xml_file_path_str → QTreeWidgetItem (für Progress Bar und Icon Updates)
        self.xml_item_map = {}

        # Theme-Menü initialisieren
        self._setup_theme_menu()

        # Vorhandene Projekte-Menü initialisieren
        self._setup_projects_menu()

        #
        if theme := app_settings.theme:
            self.change_theme(theme)
        else:
            self.change_theme("Fusion")

        # Signale und Slots verbinden
        self._connect_signals()

        # Kontextmenü für TreeWidget einrichten
        self._setup_tree_context_menu()

        # Drag&Drop für TreeWidget aktivieren
        self._setup_drag_drop()

        # Zoom per Mausrad (STRG+Mausrad) für PDF-Viewer aktivieren
        self._setup_scroll_area_zoom()

        # Icons setzen (überschreibt fromTheme()-Icons aus _ui.py). Läuft nach
        # _connect_signals(), daher existieren auch die programmatisch erzeugten Aktionen.
        self._setup_icons()
        # Ab hier reagiert das Fenster auf Theme-Wechsel (siehe IconRefreshMixin)
        self.mark_icons_ready()

        # Gespeicherte UI-Zustände wiederherstellen
        self._restore_ui_state()

    def _setup_icons(self):
        """Setzt Feather Icons für alle Menü-Aktionen und Buttons."""
        self.ui.actionNeu.setIcon(icon("folder-plus"))
        self.ui.actionBeenden.setIcon(icon("log-out"))
        self.ui.actionEinstellungen.setIcon(icon("settings"))
        self.ui.actionVorhandene_Projekte.setIcon(icon("folder"))
        self.ui.actionAlle_XML_Dateien_transformieren.setIcon(icon("play-circle"))
        self.ui.actionAlle_XML_Dateien_neu_transformieren_force.setIcon(icon("refresh-cw"))
        self.ui.actionAus_Datenbank_laden.setIcon(icon("database"))
        self.ui.view_ref_pdf.setIcon(icon("file"))
        self.ui.view_new_pdf.setIcon(icon("file"))
        self.ui.accept_changes.setIcon(icon("check-circle"))
        self.action_worker_metrics.setIcon(icon("activity"))
        self.action_xsl_dependencies.setIcon(icon("git-branch"))
        self.action_info.setIcon(icon("info"))

    def _on_icon_theme_changed(self):
        """Theme-Wechsel: Icons neu einfärben und – falls ein Projekt offen ist – die Baum-Icons."""
        self._setup_icons()
        if getattr(self, "pdf_project", None):
            self._load_nodes_to_tree()

    def _restore_ui_state(self):
        """Stellt die gespeicherten UI-Zustände wieder her (Fenstergeometrie, Splitter, TreeWidget-Spalten)."""
        global app_settings

        # Fenstergeometrie wiederherstellen
        if app_settings.window_geometry:
            x, y, width, height = app_settings.window_geometry
            self.setGeometry(x, y, width, height)

        # Splitter-Positionen wiederherstellen
        if app_settings.splitter_sizes:
            self.ui.splitter.setSizes(app_settings.splitter_sizes)

        # TreeWidget-Spaltenbreiten wiederherstellen
        if app_settings.tree_column_widths:
            for col_idx, width in enumerate(app_settings.tree_column_widths):
                if col_idx < self.ui.treeWidget.columnCount():
                    self.ui.treeWidget.setColumnWidth(col_idx, width)

    def _setup_theme_menu(self):
        """Initialisiert das Theme-Menü mit verfügbaren Themes."""
        # Hole alle verfügbaren Themes
        available_themes = QStyleFactory.keys()
        current_theme = QApplication.style().objectName()

        logger.debug(f"Verfügbare Themes: {available_themes}")
        logger.debug(f"Aktuelles Theme: {current_theme}")

        # Füge Theme-Aktionen zum Menü hinzu
        for theme_name in available_themes:
            action = QAction(theme_name, self)
            action.setCheckable(True)

            # Markiere das aktuelle Theme
            if theme_name.lower() == current_theme.lower():
                action.setChecked(True)

            # Verbinde die Aktion mit der Theme-Wechsel-Funktion
            action.triggered.connect(lambda checked, theme=theme_name: self.change_theme(theme))

            # Füge die Aktion zum Theme-Menü hinzu
            self.ui.menuThema.addAction(action)

    def _setup_projects_menu(self):
        """Initialisiert das Vorhandene Projekte-Menü mit gespeicherten Projekten."""
        # Entferne das alte Untermenü, falls vorhanden
        old_menu = self.ui.actionVorhandene_Projekte.menu()
        if old_menu:
            old_menu.clear()
            old_menu.deleteLater()

        # Prüfe ob Projekte vorhanden sind
        if not app_settings.pdf_projects:
            # Keine Projekte vorhanden - Menü deaktivieren
            self.ui.actionVorhandene_Projekte.setEnabled(False)
            self.ui.actionVorhandene_Projekte.setText("Vorhandene Projekte (keine vorhanden)")
            logger.info("Projekte-Menü deaktiviert - keine Projekte vorhanden")
            return

        # Filtere nur existierende Projekte
        valid_projects = []
        invalid_projects = []

        for project in app_settings.pdf_projects:
            if project.project_dir.exists():
                valid_projects.append(project)
            else:
                invalid_projects.append(project)
                logger.warning(f"Projekt-Verzeichnis existiert nicht: {project.name} → {project.project_dir}")

        # Wenn keine gültigen Projekte vorhanden sind
        if not valid_projects:
            self.ui.actionVorhandene_Projekte.setEnabled(False)
            self.ui.actionVorhandene_Projekte.setText("Vorhandene Projekte (keine gültigen vorhanden)")
            logger.warning(f"Projekte-Menü deaktiviert - {len(invalid_projects)} ungültige(s) Projekt(e) gefunden")
            return

        # Gültige Projekte vorhanden - Menü aktivieren und Untermenü erstellen
        self.ui.actionVorhandene_Projekte.setEnabled(True)
        self.ui.actionVorhandene_Projekte.setText("Vorhandene Projekte")

        # Erstelle ein Untermenü für die Projekte
        projects_menu = QMenu(self)

        # Füge jedes gültige Projekt als Menü-Eintrag hinzu
        for project in valid_projects:
            project_action = QAction(project.name, self)
            project_action.setCheckable(True)
            project_action.setToolTip(f"Projekt-Ordner: {project.project_dir}")

            # Markiere das aktuell geladene Projekt
            if hasattr(self, "project") and self.project and self.project.id == project.id:
                project_action.setChecked(True)

            # Verbinde die Aktion mit der Projekt-Öffnen-Funktion
            project_action.triggered.connect(lambda checked, proj=project: self.open_existing_project(proj))

            projects_menu.addAction(project_action)

        # Setze das Untermenü für die Aktion
        self.ui.actionVorhandene_Projekte.setMenu(projects_menu)

        logger.info(f"Projekte-Menü initialisiert mit {len(valid_projects)} gültigen Projekt(en)")
        if invalid_projects:
            logger.warning(f"{len(invalid_projects)} ungültige(s) Projekt(e) ausgeblendet")

    def open_existing_project(self, project: Project):
        """
        Öffnet ein vorhandenes Projekt.

        Args:
            project: Das zu öffnende PdfProject-Objekt
        """
        logger.info(f"Öffne Projekt: {project.name}")
        logger.debug(f"Projekt-Ordner: {project.project_dir}")

        # Speichere vorheriges Projekt inkl. Expand-Status (falls vorhanden)
        if hasattr(self, "project") and self.project and hasattr(self, "pdf_project") and self.pdf_project:
            try:
                logger.info(f"Speichere vorheriges Projekt: {self.project.name}")
                self._save_project_settings()
                logger.info("Vorheriges Projekt erfolgreich gespeichert")
            except Exception as e:
                logger.error(f"Fehler beim Speichern des vorherigen Projekts: {e}")

        self.project = project

        try:
            # Prüfe ob project.yaml existiert und nicht leer ist
            project_yaml_path = Path(project.project_dir) / "project.yaml"

            if project_yaml_path.exists() and project_yaml_path.stat().st_size > 0:
                # Versuche die Projekt-Einstellungen zu laden
                self.pdf_project = ProjectData.readSettings(project_dir=project.project_dir)
                logger.info(f"Projekt-Einstellungen aus {project_yaml_path} geladen!")
            else:
                # Erstelle Standard-Projekt-Einstellungen wenn Datei leer oder nicht vorhanden
                logger.warning("project.yaml ist leer oder nicht vorhanden, erstelle Standard-Einstellungen")
                self.pdf_project = ProjectData()

                # Speichere die Standard-Einstellungen in die project.yaml
                self.pdf_project.writeSettings(project_dir=project.project_dir)
                logger.info(f"Standard-Projekt-Einstellungen in {project_yaml_path} gespeichert")

            # Lade die Nodes in das TreeWidget (inkl. Diff-PDF-Counts und Icons)
            self._load_nodes_to_tree()

            # Starte Hash-Berechnung für alle XML-Dateien
            self._start_xml_hash_calculation()

            # Worker-Pools werden jetzt erst beim Start der Transformation initialisiert (lazy loading)

            # Aktiviere das Aktions-Menü, da ein Projekt geladen wurde
            self.ui.menuAktion.setEnabled(True)
            logger.info("Aktions-Menü aktiviert nach Projekt-Laden")

            # Aktualisiere das Projekte-Menü um das Häkchen beim geladenen Projekt anzuzeigen
            self._setup_projects_menu()

            # Aktualisiere das Projektpfad-Label
            self.ui.projectPath.setText(f"Projekt: {project.project_dir}")

        except Exception as e:
            logger.error(f"Fehler beim Laden des Projekts '{project.name}': {e}")
            # Fallback: Erstelle Standard-Einstellungen
            try:
                self.pdf_project = ProjectData()
                logger.info("Fallback: Standard-Projekt-Einstellungen erstellt")
                # Auch bei Fallback die Nodes laden
                self._load_nodes_to_tree()
            except Exception as fallback_error:
                logger.error(f"Fehler beim Erstellen der Fallback-Einstellungen: {fallback_error}")

    def change_theme(self, theme_name):
        """
        Wechselt das Theme der Anwendung.

        Args:
            theme_name: Name des zu verwendenden Themes
        """
        logger.info(f"Wechsle zu Theme: {theme_name}")

        try:
            # Erstelle den neuen Style
            style = QStyleFactory.create(theme_name)
            if style:
                # Wende den neuen Style auf die Anwendung an
                QApplication.setStyle(style)

                # Aktualisiere die Checkmarks im Menü
                for action in self.ui.menuThema.actions():
                    action.setChecked(action.text() == theme_name)

                logger.info(f"Theme erfolgreich gewechselt zu: {theme_name}")
                app_settings.theme = theme_name
                app_settings.save()
                # Icons/Baum werden über changeEvent (PaletteChange/StyleChange) neu eingefärbt,
                # siehe IconRefreshMixin._on_icon_theme_changed()
            else:
                logger.error(f"Fehler: Theme '{theme_name}' konnte nicht erstellt werden")

        except Exception as e:
            logger.error(f"Fehler beim Wechseln des Themes: {e}")

    def _connect_signals(self):
        """Verbindet Signale mit den entsprechenden Slots."""

        # Zoom-Slider verbinden
        self.ui.zoom.valueChanged.connect(self.apply_zoom)
        self.ui.zoom.mouseDoubleClickEvent = lambda event: self.ui.zoom.setValue(100)

        # Alpha-Slider verbinden
        self.ui.alpha.valueChanged.connect(self.on_alpha_changed)
        self.ui.alpha.mouseDoubleClickEvent = lambda event: self.ui.alpha.setValue(0)

        # Suchfeld für Tree-Filter verbinden
        self.ui.searchEdit.textChanged.connect(self._filter_tree)

        # Menü-Aktionen verbinden
        self.ui.actionNeu.triggered.connect(self.open_new_project_dialog)
        self.ui.actionEinstellungen.triggered.connect(self.open_settings_dialog)
        self.ui.actionAlle_XML_Dateien_transformieren.triggered.connect(self._transform_all_xml_files)
        self.ui.actionAlle_XML_Dateien_neu_transformieren_force.triggered.connect(self._transform_all_xml_files_force)

        # Worker-Pool-Metriken Menüeintrag (programmatisch hinzufügen)
        from PySide6.QtGui import QAction

        self.action_worker_metrics = QAction("Worker-Pool-Metriken", self)
        self.action_worker_metrics.triggered.connect(self._show_worker_pool_metrics)
        # Füge die Aktion zum Projekt-Menü hinzu (nach Einstellungen, vor Beenden)
        actions = self.ui.menuProjekt.actions()
        # Finde die Position nach actionEinstellungen
        insert_index = len(actions)  # Fallback: Am Ende
        for i, action in enumerate(actions):
            if action == self.ui.actionEinstellungen:
                insert_index = i + 1
                break
        before_action = actions[insert_index] if insert_index < len(actions) else None
        if before_action:
            self.ui.menuProjekt.insertAction(before_action, self.action_worker_metrics)
        else:
            self.ui.menuProjekt.addAction(self.action_worker_metrics)

        # Menü-Aktion "Abhängigkeitsgraph" zum Aktion-Menü hinzufügen
        self.action_xsl_dependencies = QAction("XSL-Abhängigkeitsgraph", self)
        self.action_xsl_dependencies.triggered.connect(self._show_xsl_dependency_dialog)
        self.ui.menuAktion.addSeparator()
        self.ui.menuAktion.addAction(self.action_xsl_dependencies)

        # Menü-Aktion "Aus Datenbank laden" verbinden (macht das Gleiche wie Button)
        self.ui.actionAus_Datenbank_laden.triggered.connect(self.on_load_from_fn2_clicked)

        # Button "Accept Changes" verbinden
        self.ui.accept_changes.clicked.connect(self._on_accept_changes_clicked)

        # Buttons zum Öffnen von PDFs im System-Viewer verbinden
        self.ui.view_ref_pdf.clicked.connect(self._on_view_ref_pdf_clicked)
        self.ui.view_new_pdf.clicked.connect(self._on_view_new_pdf_clicked)

        # Hilfe-Menü programmatisch erstellen
        self.menu_hilfe = QMenu("Hilfe", self)
        self.action_info = QAction("Info ...", self)
        self.action_info.triggered.connect(self._show_about_dialog)
        self.menu_hilfe.addAction(self.action_info)
        self.ui.menubar.addMenu(self.menu_hilfe)

    def open_settings_dialog(self):
        """Öffnet den Einstellungen-Dialog."""
        try:
            # Erstelle und zeige den Dialog
            dialog = AppSettingsDlg(self, app_settings)
            if dialog.exec() == AppSettingsDlg.DialogCode.Accepted:
                # Einstellungen wurden gespeichert, hier könnten weitere Aktionen folgen
                logger.info("Einstellungen wurden gespeichert")
        except Exception as e:
            logger.error(f"Fehler beim Öffnen des Einstellungen-Dialogs: {e}")

    def _show_about_dialog(self):
        """Öffnet den Info-Dialog mit Versionsinformationen und Drittanbieter-Lizenzen."""
        from ui.AboutDialog import AboutDialog

        dialog = AboutDialog(self)
        dialog.exec()

    def _show_xsl_dependency_dialog(self):
        """Öffnet den XSL-Abhängigkeitsgraph-Dialog."""
        try:
            if not self.project:
                from PySide6.QtWidgets import QMessageBox

                QMessageBox.warning(self, "Fehler", "Kein Projekt geöffnet")
                return

            xsl_dir = next((xd for xd in app_settings.xsl_dirs if xd.id == self.project.xsl_dir_id), None)
            if not xsl_dir or not xsl_dir.path_to_root_dir.exists():
                from PySide6.QtWidgets import QMessageBox

                QMessageBox.warning(self, "Fehler", "XSL-Verzeichnis nicht konfiguriert oder nicht vorhanden")
                return

            # Verwende den bestehenden Abhängigkeitsgraph (lazy init)
            if not hasattr(self, "xsl_dependency_graph") or self.xsl_dependency_graph is None:
                from xsl_dependencies import XslDependencyGraph

                self.xsl_dependency_graph = XslDependencyGraph()

            # Projekt-XSL-Pfade sammeln (absolute Pfade aller im Projekt referenzierten XSL-Dateien)
            project_xsl_paths: set[Path] = set()
            if hasattr(self, "pdf_project") and self.pdf_project is not None:
                for top_node in self.pdf_project.nodes:
                    for xsl_file in self._collect_xsl_files_recursive(top_node):
                        project_xsl_paths.add((xsl_dir.path_to_root_dir / xsl_file.xsl_file).resolve())

            from ui.XslDependencyDialog import XslDependencyDialog

            # open() statt exec() verwenden — QWebEngineView verträgt keinen verschachtelten Event-Loop
            self._xsl_dep_dialog = XslDependencyDialog(
                self, xsl_dir.path_to_root_dir, self.xsl_dependency_graph, project_xsl_paths
            )
            self._xsl_dep_dialog.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
            self._xsl_dep_dialog.open()
        except Exception as e:
            logger.error(f"Fehler beim Öffnen des Abhängigkeitsgraph-Dialogs: {e}")

    def open_new_project_dialog(self):
        """Öffnet Pdf-Projekt-Dialog."""
        try:
            # Erstelle und zeige den PdfProject-Dialog
            dialog = PdfProjectDlg(self)
            if dialog.exec() == PdfProjectDlg.DialogCode.Accepted:
                # Hole die Projektdaten aus dem Dialog
                project_data = dialog.get_project_data()

                # Erstelle neue ID für das Projekt
                new_id = max([p.id for p in app_settings.pdf_projects], default=0) + 1

                # Erstelle PdfProject-Objekt
                new_project = Project(
                    id=new_id,
                    name=project_data["name"],
                    project_dir=Path(project_data["project_dir"]),
                    java_vm_id=project_data["java_vm_id"] if project_data["java_vm_id"] != -1 else 1,
                    diff_pdf_id=project_data["diff_pdf_id"] if project_data["diff_pdf_id"] != -1 else 1,
                    saxon_jar_id=project_data["saxon_jar_id"] if project_data["saxon_jar_id"] != -1 else 1,
                    apache_fop_id=project_data["apache_fop_id"] if project_data["apache_fop_id"] != -1 else 1,
                    xsl_dir_id=project_data["xsl_dir_id"] if project_data["xsl_dir_id"] != -1 else 1,
                    postgre_sql_db_id=project_data["postgre_sql_db_id"]
                    if project_data["postgre_sql_db_id"] != -1
                    else 1,
                    fop_config_dir=Path(project_data["fop_config_dir"]) if project_data.get("fop_config_dir") else None,
                    xslt_params=project_data.get("xslt_params", {}),
                )

                # Erstelle Projekt-Ordnerstruktur
                self._create_project_structure(new_project)

                # Füge das neue Projekt zu app_settings hinzu
                app_settings.pdf_projects.append(new_project)

                # Speichere app_settings
                app_settings.save()

                logger.info(f"Neues PDF-Projekt '{project_data['name']}' wurde erstellt und gespeichert")
                logger.debug(f"Projekt-ID: {new_id}")
                logger.debug(f"Projekt-Ordner: {project_data['project_dir']}")

                # Aktualisiere das Projekte-Menü
                self._setup_projects_menu()

        except Exception as e:
            logger.error(f"Fehler beim Erstellen des neuen Projekts: {e}")

    def _create_project_structure(self, project: Project):
        """
        Erstellt die Ordnerstruktur und project.yaml-Datei für ein neues Projekt.

        Args:
            project: Das PdfProject-Objekt
        """
        try:
            project_dir = Path(project.project_dir)

            # Erstelle Unterordner
            subdirs = ["xml", "new", "diff", "ref", "tmp"]
            for subdir in subdirs:
                subdir_path = project_dir / subdir
                subdir_path.mkdir(parents=True, exist_ok=True)
                logger.debug(f"Ordner erstellt: {subdir_path}")

            project_yaml_path = project_dir / "project.yaml"

            # Erstelle Standard-Projekt-Einstellungen und speichere sie
            if not project_yaml_path.exists():
                # Erstelle Standard-PdfProjectSettings
                default_settings = ProjectData()

                # Speichere die Standard-Einstellungen in die project.yaml
                default_settings.writeSettings(project_dir=project_dir)
                logger.info(f"project.yaml mit Standard-Einstellungen erstellt: {project_yaml_path}")
            else:
                logger.debug(f"project.yaml existiert bereits: {project_yaml_path}")

        except Exception as e:
            logger.error(f"Fehler beim Erstellen der Projekt-Struktur: {e}")
            raise

    def _update_diff_icons_for_existing_pdfs(self):
        """
        Durchläuft alle XML-Items und setzt Icons für bereits existierende Diff-PDFs.
        Wird nach dem Laden eines Projekts aufgerufen.
        """
        if not hasattr(self, "project") or not self.project:
            logger.debug("Kein Projekt geladen, überspringe Diff-Icon-Update")
            return

        diff_dir = self.project.project_dir / "diff"
        if not diff_dir.exists():
            logger.debug(f"Diff-Ordner existiert nicht: {diff_dir}")
            return

        logger.info("Aktualisiere Diff-Icons für existierende PDFs...")
        logger.info(f"XML-Item-Map hat {len(self.xml_item_map)} Einträge")

        # Durchlaufe alle XML-Items in der Map
        icon_count = 0
        for map_key, tree_item in self.xml_item_map.items():
            # Map-Key hat Format "xml_path|xsl_id"
            parts = map_key.split("|")
            if len(parts) != 2:
                continue

            xml_path_str, xsl_id_str = parts
            xml_path = Path(xml_path_str)
            xml_stem = xml_path.stem

            # Diff-PDF-Dateiname: "{xml_stem}_xsl_{xsl_id_str}.pdf"
            expected_pdf = diff_dir / f"{xml_stem}_xsl_{xsl_id_str}.pdf"

            if expected_pdf.exists():
                # Icon setzen
                icon_widget = self._create_centered_diff_icon(xml_path, xsl_id_str)
                self.ui.treeWidget.setItemWidget(tree_item, 1, icon_widget)
                icon_count += 1
                logger.debug(f"Diff-Icon für existierende PDF gesetzt: {map_key}")

        logger.info(f"{icon_count} Diff-Icons für existierende PDFs gesetzt")

    def _setup_scroll_area_zoom(self):
        """Aktiviert Zoom per STRG+Mausrad für die PDF-ScrollArea."""
        try:
            # Installiere Event-Filter für scrollArea_2 (PDF-Viewer)
            self.ui.scrollArea_2.installEventFilter(self)
            logger.debug("Zoom per STRG+Mausrad für PDF-Viewer aktiviert")

        except Exception as e:
            logger.error(f"Fehler beim Aktivieren von Scroll-Area-Zoom: {e}")

    def _collect_all_diff_pdfs_under_node(
        self, node_obj, item: QTreeWidgetItem
    ) -> list[tuple[Path, str, Path, Path, Path]]:
        """
        Sammelt alle Diff-PDFs unter einem TreeNode oder XslFile.

        Args:
            node_obj: TreeNode oder XslFile Objekt
            item: Das TreeWidgetItem des Knotens

        Returns:
            list[tuple]: Liste von (xml_file_path, xsl_id_str, diff_pdf_path, ref_pdf_path, new_pdf_path)
        """
        diff_pdfs = []

        if not self.project:
            return diff_pdfs

        diff_dir = self.project.project_dir / "diff"
        ref_dir = self.project.project_dir / "ref"
        new_dir = self.project.project_dir / "new"

        if not diff_dir.exists():
            return diff_pdfs

        if isinstance(node_obj, TreeNode):
            # Sammle alle XSL/XML-Paare rekursiv
            xsl_xml_pairs = self._collect_all_xsl_xml_pairs_recursive(node_obj, item)

            for xsl_file_obj, xml_file_obj, xsl_file_item in xsl_xml_pairs:
                # Baue XML-Pfad auf
                xml_file_path = self.project.project_dir / xml_file_obj.xml

                # Baue PDF-Namen
                xml_stem = xml_file_path.stem
                xsl_id_str = "_".join(str(x) for x in xsl_file_obj.id) if xsl_file_obj.id else ""
                pdf_basename = f"{xml_stem}_xsl_{xsl_id_str}.pdf"

                diff_pdf_path = diff_dir / pdf_basename
                ref_pdf_path = ref_dir / pdf_basename
                new_pdf_path = new_dir / pdf_basename

                # Prüfe ob Diff-PDF existiert
                if diff_pdf_path.exists():
                    diff_pdfs.append((xml_file_path, xsl_id_str, diff_pdf_path, ref_pdf_path, new_pdf_path))

        elif isinstance(node_obj, XslFile):
            # Sammle alle XML-Dateien dieser XslFile
            xsl_id_str = "_".join(str(x) for x in node_obj.id) if node_obj.id else ""

            for xml_file_obj in node_obj.xmls:
                # Baue XML-Pfad auf
                xml_file_path = self.project.project_dir / xml_file_obj.xml

                # Baue PDF-Namen
                xml_stem = xml_file_path.stem
                pdf_basename = f"{xml_stem}_xsl_{xsl_id_str}.pdf"

                diff_pdf_path = diff_dir / pdf_basename
                ref_pdf_path = ref_dir / pdf_basename
                new_pdf_path = new_dir / pdf_basename

                # Prüfe ob Diff-PDF existiert
                if diff_pdf_path.exists():
                    diff_pdfs.append((xml_file_path, xsl_id_str, diff_pdf_path, ref_pdf_path, new_pdf_path))

        return diff_pdfs

    def _find_next_diff_pdf(self) -> tuple[Path, str] | None:
        """
        Findet die nächste Diff-PDF im Projekt.

        Returns:
            tuple[Path, str] | None: (xml_file_path, xsl_id_str) der nächsten Diff-PDF oder None
        """
        if not self.project:
            return None

        diff_dir = self.project.project_dir / "diff"
        if not diff_dir.exists():
            return None

        # Durchlaufe xml_item_map und prüfe welche Items Diff-PDFs haben
        for map_key, tree_item in self.xml_item_map.items():
            # Map-Key hat Format "xml_path|xsl_id"
            parts = map_key.split("|")
            if len(parts) != 2:
                continue

            xml_path_str = parts[0]
            xsl_id_str = parts[1]

            # Konvertiere zu Path
            xml_file_path = Path(xml_path_str)

            # Prüfe ob Diff-PDF existiert
            xml_stem = xml_file_path.stem
            pdf_basename = f"{xml_stem}_xsl_{xsl_id_str}.pdf"
            diff_pdf_path = diff_dir / pdf_basename

            if diff_pdf_path.exists():
                return (xml_file_path, xsl_id_str)

        return None

    def _accept_single_diff_pdf(
        self, xml_file_path: Path, xsl_id_str: str, diff_pdf_path: Path, ref_pdf_path: Path, new_pdf_path: Path
    ) -> bool:
        """
        Akzeptiert eine einzelne Diff-PDF ohne Viewer-Update.

        Args:
            xml_file_path: Pfad zur XML-Datei
            xsl_id_str: XSL-ID als String
            diff_pdf_path: Pfad zur Diff-PDF
            ref_pdf_path: Pfad zur Ref-PDF
            new_pdf_path: Pfad zur New-PDF

        Returns:
            bool: True wenn erfolgreich, False bei Fehler
        """
        pdf_basename = diff_pdf_path.name  # Initialisiere am Anfang für Exception-Handler

        try:
            # Prüfe ob new-PDF existiert
            if not new_pdf_path.exists():
                logger.warning(f"New-PDF nicht gefunden: {pdf_basename}")
                return False

            # Lösche alte ref-PDF falls vorhanden
            if ref_pdf_path.exists():
                ref_pdf_path.unlink()
                logger.debug(f"Alte Ref-PDF gelöscht: {ref_pdf_path}")

            # Verschiebe new-PDF nach ref
            shutil.move(str(new_pdf_path), str(ref_pdf_path))
            logger.debug(f"New-PDF verschoben nach Ref: {new_pdf_path} -> {ref_pdf_path}")

            # Lösche diff-PDF
            if diff_pdf_path.exists():
                diff_pdf_path.unlink()
                logger.debug(f"Diff-PDF gelöscht: {diff_pdf_path}")

            # Diff-Icon beim XML-Knoten entfernen
            # WICHTIG: xml_item_map verwendet relative Pfade, nicht absolute!
            if self.project and self.project.project_dir:
                xml_relative_path = xml_file_path.relative_to(self.project.project_dir)
            else:
                # Fallback: Verwende absoluten Pfad als String
                xml_relative_path = xml_file_path
            map_key = f"{xml_relative_path}|{xsl_id_str}"
            if map_key in self.xml_item_map:
                tree_item = self.xml_item_map[map_key]
                # Entferne Icon-Widget aus Spalte 1
                tree_item.setData(1, Qt.ItemDataRole.UserRole, None)
                self.ui.treeWidget.setItemWidget(tree_item, 1, None)

            logger.info(f"Änderungen akzeptiert für: {pdf_basename}")
            return True

        except Exception as e:
            logger.error(f"Fehler beim Akzeptieren von {pdf_basename}: {e}")
            return False

    def _accept_all_changes_under_node(self, item: QTreeWidgetItem):
        """
        Akzeptiert alle Diff-PDFs unter einem TreeNode oder XslFile.
        Leert den Viewer, falls eine der akzeptierten PDFs gerade angezeigt wird.

        Args:
            item: Das TreeWidgetItem des TreeNode oder XslFile
        """
        try:
            # Hole Node-Objekt
            node_obj = item.data(0, Qt.ItemDataRole.UserRole)
            if not node_obj:
                logger.warning("Kein Node-Objekt gefunden")
                return

            # Sammle alle Diff-PDFs unter diesem Knoten
            diff_pdfs = self._collect_all_diff_pdfs_under_node(node_obj, item)

            if not diff_pdfs:
                QMessageBox.information(self, "Info", "Keine Diff-PDFs unter diesem Knoten gefunden")
                return

            # Frage Benutzer um Bestätigung
            node_name = node_obj.bez if hasattr(node_obj, "bez") else "Unbekannt"

            reply = QMessageBox.question(
                self,
                "Alle Änderungen übernehmen",
                f"Möchten Sie wirklich alle {len(diff_pdfs)} Änderungen unter '{node_name}' übernehmen?\n\n"
                f"Dies verschiebt alle new-PDFs nach ref und löscht die diff-PDFs.",
                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                QMessageBox.StandardButton.No,
            )

            if reply != QMessageBox.StandardButton.Yes:
                return

            # Merke, ob eine der zu akzeptierenden PDFs gerade im Viewer angezeigt wird
            viewer_needs_clearing = False
            if self.current_diff_xml_path and self.current_diff_xsl_id:
                for xml_file_path, xsl_id_str, diff_pdf_path, ref_pdf_path, new_pdf_path in diff_pdfs:
                    if xml_file_path == self.current_diff_xml_path and xsl_id_str == self.current_diff_xsl_id:
                        viewer_needs_clearing = True
                        break

            # Akzeptiere alle Diff-PDFs
            successful_count = 0
            for xml_file_path, xsl_id_str, diff_pdf_path, ref_pdf_path, new_pdf_path in diff_pdfs:
                if self._accept_single_diff_pdf(xml_file_path, xsl_id_str, diff_pdf_path, ref_pdf_path, new_pdf_path):
                    successful_count += 1

            # Aktualisiere Diff-PDF-Anzahl auf übergeordneten Ebenen
            self._update_all_diff_pdf_counts()

            # Leere Viewer falls nötig
            if viewer_needs_clearing:
                self._clear_pdf_viewer()

            # Zeige Erfolgsmeldung
            if successful_count == len(diff_pdfs):
                logger.info(f"Alle {successful_count} Änderungen erfolgreich übernommen")
                QMessageBox.information(
                    self, "Erfolg", f"Alle {successful_count} Änderungen wurden erfolgreich übernommen"
                )
            else:
                failed_count = len(diff_pdfs) - successful_count
                logger.warning(
                    f"{successful_count}/{len(diff_pdfs)} Änderungen übernommen, {failed_count} fehlgeschlagen"
                )
                QMessageBox.warning(
                    self,
                    "Teilweise erfolgreich",
                    f"{successful_count} von {len(diff_pdfs)} Änderungen übernommen\n{failed_count} fehlgeschlagen",
                )

        except Exception as e:
            logger.error(f"Fehler beim Übernehmen aller Änderungen: {e}")
            QMessageBox.critical(self, "Fehler", f"Fehler beim Übernehmen der Änderungen:\n{str(e)}")

    def _on_accept_changes_clicked(self):
        """
        Handler für Accept-Changes-Button.
        Verschiebt new PDF nach ref, löscht diff PDF, aktualisiert Icons und lädt nächste Diff-PDF.
        """
        try:
            if not self.project or not self.current_diff_xml_path or not self.current_diff_xsl_id:
                logger.warning("Keine aktuelle Diff-PDF zum Akzeptieren")
                return

            # PDF-Dateinamen ermitteln
            xml_stem = self.current_diff_xml_path.stem
            pdf_basename = f"{xml_stem}_xsl_{self.current_diff_xsl_id}.pdf"

            # Pfade
            diff_dir = self.project.project_dir / "diff"
            ref_dir = self.project.project_dir / "ref"
            new_dir = self.project.project_dir / "new"

            diff_pdf_path = diff_dir / pdf_basename
            ref_pdf_path = ref_dir / pdf_basename
            new_pdf_path = new_dir / pdf_basename

            logger.info(f"Akzeptiere Änderungen für: {pdf_basename}")

            # Prüfe ob new-PDF existiert
            if not new_pdf_path.exists():
                QMessageBox.warning(self, "Fehler", f"New-PDF nicht gefunden:\n{pdf_basename}")
                return

            # Schließe alle PDF-Dokumente und leere UI VOR dem Löschen/Verschieben (wichtig für Windows)
            self._close_all_pdf_documents()

            # Entferne auch alle Widgets, die Pixmaps enthalten könnten
            self._clear_layout(self.ui.verticalLayout_2)
            self._clear_layout(self.ui.verticalLayout_3)
            self.thumbnail_to_page = {}
            self.fullsize_label = None

            logger.info("PDF-Dokumente geschlossen und UI geleert vor Dateioperationen")

            # Lösche alte ref-PDF falls vorhanden
            if ref_pdf_path.exists():
                ref_pdf_path.unlink()
                logger.info(f"Alte Ref-PDF gelöscht: {ref_pdf_path}")

            # Verschiebe new-PDF nach ref
            shutil.move(str(new_pdf_path), str(ref_pdf_path))
            logger.info(f"New-PDF verschoben nach Ref: {new_pdf_path} -> {ref_pdf_path}")

            # Lösche diff-PDF
            if diff_pdf_path.exists():
                diff_pdf_path.unlink()
                logger.info(f"Diff-PDF gelöscht: {diff_pdf_path}")

            # Diff-Icon beim aktuellen XML-Knoten entfernen
            map_key = f"{self.current_diff_xml_path}|{self.current_diff_xsl_id}"
            if map_key in self.xml_item_map:
                tree_item = self.xml_item_map[map_key]
                # Entferne Icon-Widget aus Spalte 1
                tree_item.setData(1, Qt.ItemDataRole.UserRole, None)
                self.ui.treeWidget.setItemWidget(tree_item, 1, None)
                logger.info(f"Diff-Icon entfernt für: {map_key}")

            # Diff-PDF-Anzahl auf übergeordneten Ebenen aktualisieren
            self._update_all_diff_pdf_counts()

            # Finde nächste Diff-PDF
            next_diff = self._find_next_diff_pdf()

            if next_diff:
                # Lade nächste Diff-PDF
                next_xml_path, next_xsl_id = next_diff
                logger.info(f"Lade nächste Diff-PDF: {next_xml_path} / {next_xsl_id}")
                self._load_pdf_for_comparison(next_xml_path, next_xsl_id)

                # Wähle den entsprechenden XML-Knoten im Baum aus
                map_key = f"{next_xml_path}|{next_xsl_id}"
                if map_key in self.xml_item_map:
                    tree_item = self.xml_item_map[map_key]
                    self.ui.treeWidget.setCurrentItem(tree_item)
                    self.ui.treeWidget.scrollToItem(tree_item)
                    logger.info(f"TreeWidget-Item ausgewählt: {map_key}")
            else:
                # Keine weiteren Diff-PDFs, Button deaktivieren und Viewer leeren
                logger.info("Keine weiteren Diff-PDFs vorhanden")
                self.ui.accept_changes.setEnabled(False)
                self._clear_pdf_viewer()

        except Exception as e:
            logger.error(f"Fehler beim Akzeptieren der Änderungen: {e}")
            QMessageBox.critical(self, "Fehler", f"Fehler beim Akzeptieren der Änderungen:\n{str(e)}")

    def eventFilter(self, obj, event):
        """
        Event-Filter für Zoom per STRG+Mausrad im PDF-Viewer.

        Args:
            obj: Das Objekt, das das Event erhalten hat
            event: Das Event

        Returns:
            True wenn Event behandelt wurde, sonst False
        """
        # Prüfe ob Event von scrollArea_2 kommt und ein Wheel-Event ist
        if obj == self.ui.scrollArea_2 and event.type() == QEvent.Type.Wheel:
            # Prüfe ob STRG-Taste gedrückt ist
            if event.modifiers() & Qt.KeyboardModifier.ControlModifier:
                # Zoom-Schritte: 10% pro Mausrad-Klick
                zoom_step = 10

                # Mausrad-Delta (positiv = nach oben, negativ = nach unten)
                delta = event.angleDelta().y()

                # Aktuellen Zoom-Wert holen
                current_zoom = self.ui.zoom.value()

                # Neuen Zoom-Wert berechnen
                if delta > 0:
                    # Zoom rein (rauf scrollen)
                    new_zoom = min(current_zoom + zoom_step, self.ui.zoom.maximum())
                else:
                    # Zoom raus (runter scrollen)
                    new_zoom = max(current_zoom - zoom_step, self.ui.zoom.minimum())

                # Zoom-Slider aktualisieren (triggert automatisch apply_zoom)
                self.ui.zoom.setValue(new_zoom)

                # Event als behandelt markieren
                return True

        # Für alle anderen Events: Standard-Verarbeitung
        return super().eventFilter(obj, event)

    def closeEvent(self, event):
        """Wird beim Schließen der Anwendung aufgerufen."""
        # UI-Zustände speichern
        self._save_ui_state()

        # Speichere Projekt-Einstellungen inkl. Expand-Status (falls Projekt geladen)
        if hasattr(self, "project") and self.project and hasattr(self, "pdf_project") and self.pdf_project:
            try:
                self._save_project_settings()
                logger.info("Projekt-Einstellungen beim Beenden gespeichert")
            except Exception as e:
                logger.error(f"Fehler beim Speichern der Projekt-Einstellungen: {e}")

        # Stoppe Hash-Berechnungs-Thread falls noch aktiv
        if (
            hasattr(self, "hash_calculator_thread")
            and self.hash_calculator_thread
            and self.hash_calculator_thread.isRunning()
        ):
            self.hash_calculator_thread.quit()
            self.hash_calculator_thread.wait()

        # Stoppe Transformations-Thread falls noch aktiv
        if (
            hasattr(self, "transformation_thread")
            and self.transformation_thread
            and self.transformation_thread.isRunning()
        ):
            self.transformation_thread.quit()
            self.transformation_thread.wait()

        # Beende Saxon-Worker-Pool
        self._shutdown_saxon_worker_pool()

        # Beende FOP-Worker-Pool
        self._shutdown_fop_worker_pool()

        # PDF-Dokumente schließen ist bei QtPdf automatisch durch Garbage Collection
        super().closeEvent(event)

    def _save_ui_state(self):
        """Speichert die aktuellen UI-Zustände (Fenstergeometrie, Splitter, TreeWidget-Spalten)."""
        global app_settings

        # Fenstergeometrie speichern
        geometry = self.geometry()
        app_settings.window_geometry = (geometry.x(), geometry.y(), geometry.width(), geometry.height())

        # Splitter-Positionen speichern
        app_settings.splitter_sizes = self.ui.splitter.sizes()

        # TreeWidget-Spaltenbreiten speichern
        column_count = self.ui.treeWidget.columnCount()
        app_settings.tree_column_widths = [self.ui.treeWidget.columnWidth(i) for i in range(column_count)]

        # Konfiguration speichern
        app_settings.save()
