"""
TransformationMixin - Mixin für XSL-Transformationen.

Dieses Mixin enthält alle Methoden zur Durchführung und Verwaltung von
XSL-Transformationen für das MainWindow.
"""

import logging
from copy import deepcopy
from pathlib import Path

from PySide6.QtCore import Qt
from PySide6.QtWidgets import QMessageBox, QProgressBar, QTreeWidgetItem

from conf import app_settings, TreeNode, XslFile, XmlFile
from transform import TransformationJob
from ui.threads import TransformationThread
from xsl_dependencies import XslDependencyGraph

logger = logging.getLogger(__name__)


class TransformationMixin:
    """
    Mixin für XSL-Transformationen.

    Dieses Mixin wird von MainWindow verwendet und erwartet folgende Attribute:
    - self.project: Das aktuelle Projekt
    - self.ui: Das UI-Objekt mit treeWidget, statusBar
    - self.transformation_thread: Thread für Transformationen
    - self.transformation_progress_bar: QProgressBar für Transformation-Fortschritt
    - self.xml_item_map: Mapping von xml_path|xsl_id zu TreeWidgetItems
    - self.last_saxon_metrics: Gecachte Saxon-Worker-Pool-Metriken
    - self.last_fop_metrics: Gecachte FOP-Worker-Pool-Metriken
    - self.xsl_dependency_graph: XslDependencyGraph für Import/Include-Erkennung

    Erwartet folgende Methoden von anderen Mixins:
    - self._initialize_saxon_worker_pool(): Von WorkerPoolMixin
    - self._initialize_fop_worker_pool(): Von WorkerPoolMixin
    - self._shutdown_saxon_worker_pool(): Von WorkerPoolMixin
    - self._shutdown_fop_worker_pool(): Von WorkerPoolMixin
    - self._create_centered_progress_bar(): Von TreeManagerMixin
    - self._create_centered_diff_icon(): Von TreeManagerMixin
    - self._collect_parent_params(): Von TreeManagerMixin
    - self._update_diff_icons_for_existing_pdfs(): Von MainWindow
    """

    def _show_transformation_progress_bar(self, total_jobs: int):
        """
        Zeigt einen Progressbar in der Statusbar für Transformationen.

        Args:
            total_jobs: Gesamtanzahl der Transformations-Jobs
        """
        if self.transformation_progress_bar is None:
            self.transformation_progress_bar = QProgressBar()
            self.transformation_progress_bar.setMaximumHeight(20)
            self.transformation_progress_bar.setMaximumWidth(300)

        self.transformation_progress_bar.setMinimum(0)
        self.transformation_progress_bar.setMaximum(total_jobs)
        self.transformation_progress_bar.setValue(0)
        self.transformation_progress_bar.setFormat("%v/%m Jobs")

        # Füge Progressbar zur Statusbar hinzu
        self.statusBar().addPermanentWidget(self.transformation_progress_bar)
        self.transformation_progress_bar.show()

    def _hide_transformation_progress_bar(self):
        """Versteckt und entfernt den Transformation-Progressbar aus der Statusbar."""
        if self.transformation_progress_bar:
            self.statusBar().removeWidget(self.transformation_progress_bar)
            self.transformation_progress_bar.hide()

    def _update_transformation_progress(self):
        """Aktualisiert den Transformation-Progressbar um einen Schritt."""
        if self.transformation_progress_bar:
            current_value = self.transformation_progress_bar.value()
            self.transformation_progress_bar.setValue(current_value + 1)

    def _transform_xml_file(self, item: QTreeWidgetItem, force: bool = False):
        """
        Transformiert eine einzelne XML-Datei.

        Args:
            item: Das TreeWidgetItem der XML-Datei
            force: Wenn True, wird Transformation auch bei aktuellem Output durchgeführt
        """
        try:
            # Hole XslFile vom Parent-Item
            parent_item = item.parent()
            if not parent_item:
                logger.error("XML-Datei hat kein Parent-Item (XslFile)")
                QMessageBox.warning(self, "Fehler", "XML-Datei hat keine zugeordnete XSL-Datei")
                return

            xsl_file_obj = parent_item.data(0, Qt.ItemDataRole.UserRole)
            if not isinstance(xsl_file_obj, XslFile):
                logger.error(f"Parent-Item ist kein XslFile: {type(xsl_file_obj)}")
                QMessageBox.warning(self, "Fehler", "Konnte XSL-Datei nicht ermitteln")
                return

            # Hole XmlFile-Objekt
            xml_file_obj = item.data(0, Qt.ItemDataRole.UserRole)
            if not isinstance(xml_file_obj, XmlFile):
                logger.error(f"Item ist kein XmlFile: {type(xml_file_obj)}")
                QMessageBox.warning(self, "Fehler", "Konnte XML-Datei nicht ermitteln")
                return

            # Erstelle TransformationJob mit TreeWidgetItem-Kontext für Parameter-Sammlung
            job = self._create_transformation_job(xsl_file_obj, xml_file_obj, parent_item)
            if not job:
                QMessageBox.warning(
                    self,
                    "Fehler",
                    "Transformation kann nicht gestartet werden.\n\n"
                    "Bitte überprüfen Sie, ob XML- und XSL-Dateien existieren.\n"
                    "Details finden Sie im Log.",
                )
                return

            # Starte Transformation in separatem Thread
            self._start_transformation([job], force=force)

        except Exception as e:
            logger.error(f"Fehler beim Transformieren der XML-Datei: {e}")
            QMessageBox.critical(self, "Fehler", f"Fehler beim Transformieren: {str(e)}")

    def _transform_xsl_file(self, item: QTreeWidgetItem, force: bool = False):
        """
        Transformiert alle XML-Dateien einer XSL-Datei.

        Args:
            item: Das TreeWidgetItem der XSL-Datei
            force: Wenn True, wird Transformation auch bei aktuellem Output durchgeführt
        """
        try:
            # Hole XslFile-Objekt
            xsl_file_obj = item.data(0, Qt.ItemDataRole.UserRole)
            if not isinstance(xsl_file_obj, XslFile):
                logger.error(f"Item ist kein XslFile: {type(xsl_file_obj)}")
                QMessageBox.warning(self, "Fehler", "Konnte XSL-Datei nicht ermitteln")
                return

            # Prüfe ob XML-Dateien vorhanden sind
            if not xsl_file_obj.xmls:
                QMessageBox.information(self, "Info", "Keine XML-Dateien zugeordnet")
                return

            # Erstelle TransformationJobs für alle XML-Dateien
            jobs = []
            skipped_count = 0
            for xml_file_obj in xsl_file_obj.xmls:
                # Übergebe das XslFile-TreeWidgetItem für Parameter-Sammlung
                job = self._create_transformation_job(xsl_file_obj, xml_file_obj, item)
                if job:
                    jobs.append(job)
                else:
                    skipped_count += 1

            if not jobs:
                QMessageBox.warning(
                    self,
                    "Fehler",
                    f"Konnte keine gültigen Transformations-Jobs erstellen.\n\n"
                    f"{skipped_count} XML-Datei(en) wurden übersprungen (fehlende Dateien).",
                )
                return

            # Informiere Benutzer wenn einige Jobs übersprungen wurden
            if skipped_count > 0:
                logger.warning(f"{skipped_count} von {len(xsl_file_obj.xmls)} Jobs übersprungen (fehlende Dateien)")
                QMessageBox.warning(
                    self,
                    "Warnung",
                    f"{skipped_count} von {len(xsl_file_obj.xmls)} XML-Datei(en) werden übersprungen "
                    f"(fehlende XML- oder XSL-Dateien).\n\n"
                    f"{len(jobs)} Job(s) werden ausgeführt.",
                )

            # Starte Transformation in separatem Thread
            self._start_transformation(jobs, force=force)

        except Exception as e:
            logger.error(f"Fehler beim Transformieren der XSL-Datei: {e}")
            QMessageBox.critical(self, "Fehler", f"Fehler beim Transformieren: {str(e)}")

    def _count_diff_pdfs_under_node(self, node: TreeNode | XslFile, node_item: QTreeWidgetItem) -> int:
        """
        Zählt die Anzahl der existierenden Diff-PDFs unter einem Knoten.

        Args:
            node: TreeNode oder XslFile Objekt
            node_item: Das TreeWidgetItem des Knotens

        Returns:
            int: Anzahl der existierenden Diff-PDF-Dateien
        """
        count = 0

        if isinstance(node, XslFile):
            # Für XslFile: Zähle Diff-PDFs für jede XML-Datei
            if not self.project:
                return 0

            diff_dir = self.project.project_dir / "diff"
            xsl_id_str = "_".join(str(x) for x in node.id) if node.id else ""

            for xml_file_obj in node.xmls:
                xml_stem = xml_file_obj.xml.stem
                pdf_basename = f"{xml_stem}_xsl_{xsl_id_str}.pdf"
                diff_pdf_path = diff_dir / pdf_basename

                if diff_pdf_path.exists():
                    count += 1

        elif isinstance(node, TreeNode):
            # Für TreeNode: Rekursiv alle Kinder durchgehen
            for i in range(node_item.childCount()):
                child_item = node_item.child(i)
                child_node = child_item.data(0, Qt.ItemDataRole.UserRole)

                if isinstance(child_node, (XslFile, TreeNode)):
                    count += self._count_diff_pdfs_under_node(child_node, child_item)

        return count

    def _update_diff_pdf_counts_recursive(self, tree_item: QTreeWidgetItem):
        """
        Aktualisiert rekursiv die Diff-PDF-Anzahl in Spalte 1 für alle TreeNode und XslFile Items.

        Args:
            tree_item: Das TreeWidgetItem (kann Root oder beliebiger Knoten sein)
        """
        node = tree_item.data(0, Qt.ItemDataRole.UserRole)

        # Aktualisiere nur für TreeNode und XslFile, nicht für XmlFile
        if isinstance(node, (TreeNode, XslFile)):
            count = self._count_diff_pdfs_under_node(node, tree_item)
            tree_item.setText(1, str(count) if count > 0 else "")

        # Rekursiv für alle Kinder
        for i in range(tree_item.childCount()):
            child_item = tree_item.child(i)
            self._update_diff_pdf_counts_recursive(child_item)

    def _update_all_diff_pdf_counts(self):
        """
        Aktualisiert die Diff-PDF-Anzahl für alle Knoten im TreeWidget.
        """
        root = self.ui.treeWidget.invisibleRootItem()
        for i in range(root.childCount()):
            self._update_diff_pdf_counts_recursive(root.child(i))

    def _has_xml_files_recursive(self, node: TreeNode) -> bool:
        """
        Prüft rekursiv, ob unter einem TreeNode mindestens eine XML-Datei vorhanden ist.

        Args:
            node: Der TreeNode

        Returns:
            bool: True wenn mindestens eine XML-Datei gefunden wurde
        """
        if not hasattr(node, "children") or not node.children:
            return False

        for child in node.children:
            if isinstance(child, XslFile):
                if child.xmls:
                    return True
            elif isinstance(child, TreeNode):
                if self._has_xml_files_recursive(child):
                    return True

        return False

    def _collect_all_xsl_xml_pairs_recursive(
        self, tree_node: TreeNode, tree_item: QTreeWidgetItem
    ) -> list[tuple[XslFile, XmlFile, QTreeWidgetItem]]:
        """
        Sammelt rekursiv alle (XslFile, XmlFile, XslFileItem) Tupel unter einem TreeNode.

        Args:
            tree_node: Der TreeNode
            tree_item: Das TreeWidgetItem des TreeNode

        Returns:
            list: Liste von (XslFile, XmlFile, XslFileItem) Tupeln
        """
        pairs = []

        if not hasattr(tree_node, "children") or not tree_node.children:
            return pairs

        # Durchlaufe alle Kinder des TreeNode
        for i in range(tree_item.childCount()):
            child_item = tree_item.child(i)
            child_node = child_item.data(0, Qt.ItemDataRole.UserRole)

            if isinstance(child_node, XslFile):
                # XslFile gefunden - sammle alle XML-Dateien
                for xml_file_obj in child_node.xmls:
                    pairs.append((child_node, xml_file_obj, child_item))

            elif isinstance(child_node, TreeNode):
                # Rekursiv in Unterknoten suchen
                pairs.extend(self._collect_all_xsl_xml_pairs_recursive(child_node, child_item))

        return pairs

    def _transform_tree_node(self, item: QTreeWidgetItem, force: bool = False):
        """
        Transformiert alle XML-Dateien unter einem TreeNode (rekursiv).

        Args:
            item: Das TreeWidgetItem des TreeNode
            force: Wenn True, wird Transformation auch bei aktuellem Output durchgeführt
        """
        try:
            # Hole TreeNode-Objekt
            tree_node_obj = item.data(0, Qt.ItemDataRole.UserRole)
            if not isinstance(tree_node_obj, TreeNode):
                logger.error(f"Item ist kein TreeNode: {type(tree_node_obj)}")
                QMessageBox.warning(self, "Fehler", "Konnte TreeNode nicht ermitteln")
                return

            # Prüfe ob XML-Dateien vorhanden sind
            if not self._has_xml_files_recursive(tree_node_obj):
                QMessageBox.information(self, "Info", "Keine XML-Dateien unter diesem Knoten gefunden")
                return

            # Sammle alle XSL/XML-Paare rekursiv
            xsl_xml_pairs = self._collect_all_xsl_xml_pairs_recursive(tree_node_obj, item)

            if not xsl_xml_pairs:
                QMessageBox.information(self, "Info", "Keine XML-Dateien gefunden")
                return

            # Erstelle TransformationJobs für alle XML-Dateien
            jobs = []
            skipped_count = 0
            for xsl_file_obj, xml_file_obj, xsl_file_item in xsl_xml_pairs:
                # Übergebe das XslFile-TreeWidgetItem für Parameter-Sammlung
                job = self._create_transformation_job(xsl_file_obj, xml_file_obj, xsl_file_item)
                if job:
                    jobs.append(job)
                else:
                    skipped_count += 1

            if not jobs:
                QMessageBox.warning(
                    self,
                    "Fehler",
                    f"Konnte keine gültigen Transformations-Jobs erstellen.\n\n"
                    f"{skipped_count} XML-Datei(en) wurden übersprungen (fehlende Dateien).",
                )
                return

            # Informiere Benutzer wenn einige Jobs übersprungen wurden
            if skipped_count > 0:
                logger.warning(f"{skipped_count} von {len(xsl_xml_pairs)} Jobs übersprungen (fehlende Dateien)")
                QMessageBox.warning(
                    self,
                    "Warnung",
                    f"{skipped_count} von {len(xsl_xml_pairs)} XML-Datei(en) werden übersprungen "
                    f"(fehlende XML- oder XSL-Dateien).\n\n"
                    f"{len(jobs)} Job(s) werden ausgeführt.",
                )

            logger.info(f"Starte Transformation für {len(jobs)} XML-Dateien unter TreeNode '{tree_node_obj.bez}'")

            # Starte Transformation in separatem Thread
            self._start_transformation(jobs, force=force)

        except Exception as e:
            logger.error(f"Fehler beim Transformieren des TreeNode: {e}")
            QMessageBox.critical(self, "Fehler", f"Fehler beim Transformieren: {str(e)}")

    def _get_cached_project_tools(self):
        """
        Gibt die aufgelösten Tool-Konfigurationen des aktuellen Projekts zurück (gecacht).

        Der Cache wird invalidiert, sobald sich die Projekt-ID ändert.
        """
        project_id = self.project.id if self.project else None
        if getattr(self, "_tool_cache_project_id", None) != project_id:
            self._tool_cache_project_id = project_id
            self._tool_cache = (
                next((jvm for jvm in app_settings.java_vms if jvm.id == self.project.java_vm_id), None),
                next((sj for sj in app_settings.saxon_jars if sj.id == self.project.saxon_jar_id), None),
                next((af for af in app_settings.apache_fops if af.id == self.project.apache_fop_id), None),
                next((dp for dp in app_settings.diff_pdfs if dp.id == self.project.diff_pdf_id), None),
                next((xd for xd in app_settings.xsl_dirs if xd.id == self.project.xsl_dir_id), None),
            )
        return self._tool_cache

    def _create_transformation_job(
        self, xsl_file_obj: XslFile, xml_file_obj: XmlFile, xsl_file_item: QTreeWidgetItem | None = None
    ) -> TransformationJob | None:
        """
        Erstellt einen TransformationJob für eine XML/XSL-Kombination.

        Args:
            xsl_file_obj: Das XslFile-Objekt
            xml_file_obj: Das XmlFile-Objekt
            xsl_file_item: Optional das TreeWidgetItem des XslFile für hierarchische Parameter-Sammlung

        Returns:
            TransformationJob oder None bei Fehler
        """
        try:
            if not self.project:
                QMessageBox.warning(self, "Fehler", "Kein Projekt geöffnet")
                return None

            # Tool-Konfigurationen gecacht auflösen (einmalig pro Projekt)
            java_vm, saxon_jar, apache_fop, diff_pdf, xsl_dir = self._get_cached_project_tools()

            # Prüfe ob alle Konfigurationen vorhanden sind
            if not all([java_vm, saxon_jar, apache_fop, diff_pdf, xsl_dir]):
                missing = []
                if not java_vm:
                    missing.append("Java VM")
                if not saxon_jar:
                    missing.append("Saxon JAR")
                if not apache_fop:
                    missing.append("Apache FOP")
                if not diff_pdf:
                    missing.append("diff-pdf")
                if not xsl_dir:
                    missing.append("XSL-Verzeichnis")

                QMessageBox.warning(
                    self, "Fehlende Konfiguration", f"Folgende Konfigurationen fehlen: {', '.join(missing)}"
                )
                return None

            # Zusätzliche Sicherheitsprüfung für path_to_binary_file Attribute
            if java_vm is None or not hasattr(java_vm, "path_to_binary_file") or java_vm.path_to_binary_file is None:
                QMessageBox.warning(self, "Konfigurationsfehler", "Java VM Pfad ist nicht konfiguriert")
                return None

            if saxon_jar is None or not hasattr(saxon_jar, "path_to_jar_file") or saxon_jar.path_to_jar_file is None:
                QMessageBox.warning(self, "Konfigurationsfehler", "Saxon JAR Pfad ist nicht konfiguriert")
                return None

            if apache_fop is None or not hasattr(apache_fop, "path_to_dir") or apache_fop.path_to_dir is None:
                QMessageBox.warning(self, "Konfigurationsfehler", "Apache FOP Pfad ist nicht konfiguriert")
                return None

            if diff_pdf is None or not hasattr(diff_pdf, "path_to_binary_file") or diff_pdf.path_to_binary_file is None:
                QMessageBox.warning(self, "Konfigurationsfehler", "diff-pdf Pfad ist nicht konfiguriert")
                return None

            if xsl_dir is None or not hasattr(xsl_dir, "path_to_root_dir") or xsl_dir.path_to_root_dir is None:
                QMessageBox.warning(self, "Konfigurationsfehler", "XSL-Verzeichnis Pfad ist nicht konfiguriert")
                return None

            # Erstelle absoluten Pfad zur XSL-Datei
            xsl_file_abs = xsl_dir.path_to_root_dir / xsl_file_obj.xsl_file

            # Prüfe ob XSL-Datei existiert
            if not xsl_file_abs.exists():
                error_msg = f"XSL-Datei nicht gefunden: {xsl_file_abs}"
                logger.error(error_msg)
                # Kein MessageBox hier - wird in der aufrufenden Funktion behandelt
                return None

            # Erstelle absoluten Pfad zur XML-Datei
            xml_file_abs = self.project.project_dir / xml_file_obj.xml

            # Prüfe ob XML-Datei existiert
            if not xml_file_abs.exists():
                error_msg = f"XML-Datei nicht gefunden: {xml_file_abs}"
                logger.error(error_msg)
                # Kein MessageBox hier - wird in der aufrufenden Funktion behandelt
                return None

            # Sammle XSLT-Parameter hierarchisch (TreeNode-Eltern → XslFile)
            xslt_params = {}

            # 1. Sammle Parameter von übergeordneten TreeNodes (falls TreeWidgetItem verfügbar)
            if xsl_file_item is not None:
                parent_params = self._collect_parent_params(xsl_file_item)
                xslt_params.update(parent_params)
                logger.debug(f"Hierarchische Parameter gesammelt: {parent_params}")
            else:
                # Ohne TreeWidgetItem-Kontext: nur Projekt-Parameter als Basis
                if hasattr(self, "project") and self.project and self.project.xslt_params:
                    xslt_params.update(self.project.xslt_params)
                logger.warning(
                    "Kein TreeWidgetItem-Kontext verfügbar - "
                    "übergeordnete TreeNode-Parameter werden nicht berücksichtigt"
                )

            # 2. Überschreibe mit XslFile-eigenen Parametern (höchste Priorität)
            xslt_params.update(xsl_file_obj.xslt_params)

            logger.info(f"Finale XSLT-Parameter für {xml_file_obj.xml} mit {xsl_file_obj.bez}: {xslt_params}")

            # Initialisiere XSL-Abhängigkeitsgraph (lazy, einmalig pro Mixin-Instanz)
            if not hasattr(self, "xsl_dependency_graph") or self.xsl_dependency_graph is None:
                self.xsl_dependency_graph = XslDependencyGraph()

            # Erstelle TransformationJob
            job = TransformationJob(
                project_dir=self.project.project_dir,
                xml_file=xml_file_obj.xml,
                xsl_file=xsl_file_abs,
                xslt_params=xslt_params,
                java_vm_path=java_vm.path_to_binary_file,
                saxon_jar_path=saxon_jar.path_to_jar_file,
                apache_fop_dir=apache_fop.path_to_dir,
                diff_pdf_path=diff_pdf.path_to_binary_file,
                diff_pdf_params=diff_pdf.default_params,
                xsl_id=xsl_file_obj.id,
                fop_config_dir=self.project.fop_config_dir,
                dependency_graph=self.xsl_dependency_graph,
            )

            return job

        except Exception as e:
            logger.error(f"Fehler beim Erstellen des TransformationJobs: {e}")
            QMessageBox.critical(self, "Fehler", f"Fehler beim Erstellen des Jobs: {str(e)}")
            return None

    def _start_transformation(self, jobs: list[TransformationJob], force: bool = False):
        """
        Startet die Transformation in einem separaten Thread.

        Args:
            jobs: Liste der TransformationJobs
            force: Wenn True, werden alle Jobs ausgeführt (ignoriert Up-to-Date)
        """
        try:
            # Prüfe ob bereits ein Thread läuft
            if self.transformation_thread and self.transformation_thread.isRunning():
                QMessageBox.warning(self, "Warnung", "Es läuft bereits eine Transformation")
                return

            # Erstelle und konfiguriere Thread
            self.transformation_thread = TransformationThread(jobs, force=force, max_workers=app_settings.max_workers)

            # Verbinde Signale
            self.transformation_thread.job_started.connect(self._on_transformation_job_started)
            self.transformation_thread.job_finished.connect(self._on_transformation_job_finished)
            self.transformation_thread.job_error.connect(self._on_transformation_job_error)
            self.transformation_thread.all_jobs_finished.connect(self._on_all_transformations_finished)

            # Zeige Progressbar
            self._show_transformation_progress_bar(len(jobs))

            # Initialisiere Worker-Pools (lazy loading - nur wenn benötigt)
            self._initialize_saxon_worker_pool()
            self._initialize_fop_worker_pool()

            # Erfasse RAM-Verbrauch vor Transformation
            import transform

            if transform._saxon_worker_pool:
                transform._saxon_worker_pool.capture_ram_before_transform()
            if transform._fop_worker_pool:
                transform._fop_worker_pool.capture_ram_before_transform()

            # Starte Thread
            self.transformation_thread.start()

            logger.info(f"Transformation von {len(jobs)} Job(s) gestartet (force={force})")
            self.statusBar().showMessage(f"Transformation von {len(jobs)} Job(s) gestartet...")

        except Exception as e:
            logger.error(f"Fehler beim Starten der Transformation: {e}")
            QMessageBox.critical(self, "Fehler", f"Fehler beim Starten: {str(e)}")

    def _expand_tree_item_parents(self, item: QTreeWidgetItem):
        """
        Öffnet alle Eltern-Knoten eines Tree-Items rekursiv.

        Args:
            item: Das Tree-Item, dessen Eltern geöffnet werden sollen
        """
        if item is None:
            return

        # Rekursiv alle Eltern öffnen
        parent = item.parent()
        while parent is not None:
            parent.setExpanded(True)
            parent = parent.parent()

    def _on_transformation_job_started(self, xml_file_name: str, xsl_id_str: str):
        """
        Signal-Handler: Ein Job wurde gestartet.

        Args:
            xml_file_name: Name der XML-Datei
            xsl_id_str: XSL-ID als String (z.B. "2002_1_128")
        """
        logger.info(f"Transformation gestartet: {xml_file_name} (XSL-ID: {xsl_id_str})")
        self.statusBar().showMessage(f"Transformiere: {xml_file_name}")

        # Progress Bar anzeigen
        map_key = f"{xml_file_name}|{xsl_id_str}"
        logger.info(f"Suche TreeWidget-Item für: '{map_key}'")
        logger.info(f"Map hat {len(self.xml_item_map)} Einträge")
        tree_item = self.xml_item_map.get(map_key)
        if tree_item:
            # Öffne alle Eltern-Knoten, damit der Benutzer den Fortschritt sehen kann
            self._expand_tree_item_parents(tree_item)

            # Scrolle zum Item, damit es sichtbar ist
            self.ui.treeWidget.scrollToItem(tree_item)

            # Entferne vorhandenes Widget (falls Icon vorhanden)
            self.ui.treeWidget.removeItemWidget(tree_item, 1)

            # Erstelle und setze Progress Bar
            progress_widget, progress_bar = self._create_centered_progress_bar()
            self.ui.treeWidget.setItemWidget(tree_item, 1, progress_widget)

            logger.debug(f"Progress Bar für {xml_file_name} gesetzt und Eltern-Knoten geöffnet")
        else:
            logger.warning(f"Kein TreeWidget-Item für {xml_file_name} gefunden")

    def _on_transformation_job_finished(self, result: dict):
        """
        Signal-Handler: Ein Job wurde abgeschlossen.

        Args:
            result: Ergebnis-Dictionary
        """
        # Aktualisiere Transformation-Progressbar
        self._update_transformation_progress()

        xml_file = result.get("xml_file", "?")
        success = result.get("success", False)
        duration = result.get("duration", 0)

        if success:
            logger.info(f"Transformation erfolgreich: {xml_file} ({duration:.2f}s)")
            pdfs_identical = result.get("pdfs_identical", False)
            if pdfs_identical:
                self.statusBar().showMessage(f"✓ {xml_file} - PDFs identisch ({duration:.2f}s)", 3000)
            else:
                self.statusBar().showMessage(f"⚠ {xml_file} - Unterschiede gefunden ({duration:.2f}s)", 3000)
        else:
            logger.error(f"Transformation fehlgeschlagen: {xml_file}")
            # Zeige Fehlerdetails an
            steps = result.get("steps", {})
            error_msgs = []
            for step_name, step_info in steps.items():
                if not step_info.get("success", True):
                    error_msgs.append(f"{step_name}: {step_info.get('message', 'Unbekannter Fehler')}")

            error_text = "\n".join(error_msgs) if error_msgs else "Unbekannter Fehler"
            QMessageBox.critical(
                self, "Transformation fehlgeschlagen", f"XML-Datei: {xml_file}\n\nFehler:\n{error_text}"
            )

        # Update Widget in Spalte 1: Entferne Progress Bar, zeige Icon wenn Diff-PDF existiert
        xml_file_str = result.get("xml_file", "")
        xsl_id = result.get("xsl_id", None)
        xsl_id_str = "_".join(str(x) for x in xsl_id) if xsl_id else ""
        map_key = f"{xml_file_str}|{xsl_id_str}"
        diff_pdf_str = result.get("diff_pdf", None)
        tree_item = self.xml_item_map.get(map_key)

        if tree_item:
            # Entferne Progress Bar
            self.ui.treeWidget.removeItemWidget(tree_item, 1)

            # Wenn Diff-PDF existiert, zeige Icon
            if diff_pdf_str and Path(diff_pdf_str).exists():
                xml_file_path = Path(xml_file_str)
                icon_widget = self._create_centered_diff_icon(xml_file_path, xsl_id_str)
                self.ui.treeWidget.setItemWidget(tree_item, 1, icon_widget)
                logger.debug(f"Diff-Icon für {xml_file_str} gesetzt")
            else:
                logger.debug(f"Keine Diff-PDF für {xml_file_str}, kein Icon gesetzt")

    def _on_transformation_job_error(self, xml_file_name: str, xsl_id_str: str, error_message: str):
        """
        Signal-Handler: Ein Job ist mit einem Fehler abgebrochen.

        Args:
            xml_file_name: Name der XML-Datei
            xsl_id_str: XSL-ID als String
            error_message: Fehlermeldung
        """
        # Aktualisiere Transformation-Progressbar
        self._update_transformation_progress()

        logger.error(f"Transformation-Fehler bei {xml_file_name} (XSL-ID: {xsl_id_str}): {error_message}")
        QMessageBox.critical(self, "Fehler", f"Fehler bei {xml_file_name}:\n{error_message}")

        # Entferne Progress Bar bei Fehler
        map_key = f"{xml_file_name}|{xsl_id_str}"
        tree_item = self.xml_item_map.get(map_key)
        if tree_item:
            self.ui.treeWidget.removeItemWidget(tree_item, 1)
            logger.debug(f"Progress Bar für {map_key} entfernt (Fehler)")

    def _on_all_transformations_finished(self, successful_count: int, total_count: int, total_duration: float):
        """
        Signal-Handler: Alle Jobs wurden abgeschlossen.

        Args:
            successful_count: Anzahl erfolgreicher Jobs
            total_count: Gesamtanzahl der Jobs
            total_duration: Gesamtdauer aller Transformationen in Sekunden
        """
        logger.info(
            f"Alle Transformationen abgeschlossen: {successful_count}/{total_count} erfolgreich ({total_duration:.2f}s)"
        )

        # Erfasse RAM-Verbrauch nach Transformation
        import transform

        if transform._saxon_worker_pool:
            transform._saxon_worker_pool.capture_ram_after_transform()
            # Speichere Metriken vor Shutdown (für späteren Zugriff im Dialog)
            self.last_saxon_metrics = deepcopy(transform._saxon_worker_pool.metrics)
            logger.debug("Saxon Worker-Pool Metriken gespeichert")

        if transform._fop_worker_pool:
            transform._fop_worker_pool.capture_ram_after_transform()
            # Speichere Metriken vor Shutdown (für späteren Zugriff im Dialog)
            self.last_fop_metrics = deepcopy(transform._fop_worker_pool.metrics)
            logger.debug("FOP Worker-Pool Metriken gespeichert")

        # Beende Worker-Pools (RAM-Optimierung - Pools werden bei nächster Transformation neu gestartet)
        self._shutdown_saxon_worker_pool()
        self._shutdown_fop_worker_pool()

        # Verstecke Transformation-Progressbar
        self._hide_transformation_progress_bar()

        # Aktualisiere Diff-PDF-Anzahl und Icons in allen Knoten
        self._update_all_diff_pdf_counts()
        self._update_diff_icons_for_existing_pdfs()

        # Formatiere Dauer für Anzeige
        duration_str = f"{total_duration:.2f}s"

        if successful_count == total_count:
            self.statusBar().showMessage(f"✓ Alle {total_count} Transformationen erfolgreich ({duration_str})", 5000)
            QMessageBox.information(
                self,
                "Abgeschlossen",
                f"Alle {total_count} Transformationen wurden erfolgreich abgeschlossen.\n\nGesamtdauer: {duration_str}",
            )
        else:
            failed_count = total_count - successful_count
            self.statusBar().showMessage(
                f"⚠ {successful_count}/{total_count} erfolgreich, {failed_count} fehlgeschlagen ({duration_str})", 5000
            )
            QMessageBox.warning(
                self,
                "Abgeschlossen mit Fehlern",
                f"{successful_count} von {total_count} Transformationen erfolgreich\n{failed_count} fehlgeschlagen\n\nGesamtdauer: {duration_str}",
            )

    def _transform_all_xml_files(self, force: bool = False):
        """
        Transformiert ALLE XML-Dateien in allen TreeNodes des TreeWidgets.

        Args:
            force: Wenn True, werden alle Dateien unabhängig vom Änderungsstatus neu transformiert.
        """
        try:
            if not self.project or not self.pdf_project:
                QMessageBox.warning(self, "Fehler", "Kein Projekt geladen")
                return

            # Sammle alle XSL/XML-Paare aus allen Root-Nodes
            all_jobs = []
            root = self.ui.treeWidget.invisibleRootItem()

            for i in range(root.childCount()):
                root_item = root.child(i)
                root_node = root_item.data(0, Qt.ItemDataRole.UserRole)

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

                    # Erstelle TransformationJobs
                    for xsl_file_obj, xml_file_obj, xsl_file_item in xsl_xml_pairs:
                        job = self._create_transformation_job(xsl_file_obj, xml_file_obj, xsl_file_item)
                        if job:
                            all_jobs.append(job)

                elif isinstance(root_node, XslFile):
                    # Direkt XslFile als Root-Element
                    for xml_file_obj in root_node.xmls:
                        job = self._create_transformation_job(root_node, xml_file_obj, root_item)
                        if job:
                            all_jobs.append(job)

            if not all_jobs:
                QMessageBox.information(self, "Info", "Keine XML-Dateien zum Transformieren gefunden")
                return

            # Frage Benutzer um Bestätigung
            if force:
                title = "Alle XML-Dateien neu transformieren (force)"
                message = (
                    f"Möchten Sie wirklich ALLE {len(all_jobs)} XML-Dateien NEU transformieren?\n\n"
                    f"⚠ WARNUNG: Im Force-Modus werden alle Dateien unabhängig von ihrem Status neu verarbeitet!\n"
                    f"Dies kann längere Zeit in Anspruch nehmen."
                )
                default_button = QMessageBox.StandardButton.No
            else:
                title = "Alle XML-Dateien transformieren"
                message = (
                    f"Möchten Sie wirklich alle {len(all_jobs)} XML-Dateien transformieren?\n\n"
                    f"Nur Dateien mit Änderungen werden verarbeitet."
                )
                default_button = QMessageBox.StandardButton.Yes

            reply = QMessageBox.question(
                self,
                title,
                message,
                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                default_button,
            )

            if reply != QMessageBox.StandardButton.Yes:
                logger.info(f"{'Force-' if force else ''}Transformation abgebrochen durch Benutzer")
                return

            logger.info(f"Starte {'Force-' if force else ''}Transformation für alle {len(all_jobs)} XML-Dateien")

            # Starte Transformation in separatem Thread
            self._start_transformation(all_jobs, force=force)

        except Exception as e:
            logger.error(f"Fehler beim Transformieren aller XML-Dateien: {e}")
            QMessageBox.critical(self, "Fehler", f"Fehler beim Starten der Transformation: {str(e)}")

    def _transform_all_xml_files_force(self):
        """Transformiert ALLE XML-Dateien im Force-Modus."""
        self._transform_all_xml_files(force=True)
