"""
TreeManagerMixin - Mixin für Tree-Widget-Operationen.

Dieses Mixin enthält alle Methoden für die Verwaltung des TreeWidgets im MainWindow:
- Setup und Styling
- Kontextmenüs
- Node-Operationen (Hinzufügen, Bearbeiten, Löschen)
- Tree-Navigation und -Suche
"""

import logging
import shutil
import time
from enum import Enum, auto
from pathlib import Path

from PySide6.QtCore import Qt
from PySide6.QtGui import QAction
from PySide6.QtWidgets import (
    QMenu,
    QTreeWidgetItem,
    QMessageBox,
    QFileDialog,
    QWidget,
    QHBoxLayout,
    QLabel,
    QProgressBar,
)

from conf import TreeNode, XslFile, XmlFile
from icons import icon
from ui.TreeNodeEditDialog import TreeNodeEditDialog
from ui.XslFileEditDialog import XslFileEditDialog
from ui.XmlToXslAssignDialog import XmlToXslAssignDialog


class ItemType(Enum):
    TREE_NODE = auto()
    XSL_FILE = auto()
    XML_FILE = auto()
    UNKNOWN = auto()


logger = logging.getLogger(__name__)


class TreeManagerMixin:
    """
    Mixin-Klasse für Tree-Widget-Operationen.

    Dieses Mixin erwartet, dass die verwendende Klasse folgende Attribute hat:
    - self.ui: UI-Objekt mit treeWidget
    - self.project: Aktuelles Projekt
    - self.pdf_project: Projekt-Daten
    - self.xml_item_map: Dict für XML-Item-Mapping
    """

    def _setup_tree_context_menu(self):
        """Richtet das Kontextmenü für das TreeWidget ein."""
        # Aktiviere Kontextmenü für das TreeWidget
        self.ui.treeWidget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.ui.treeWidget.customContextMenuRequested.connect(self._show_tree_context_menu)
        self.ui.treeWidget.itemDoubleClicked.connect(self._on_tree_item_double_clicked)

        # Verbinde Selection-Changed-Signal für automatisches Laden von Diff-PDFs
        self.ui.treeWidget.itemSelectionChanged.connect(self._on_tree_selection_changed)

        logger.debug("Kontextmenü und Selection-Handler für TreeWidget eingerichtet")

    def _on_tree_item_double_clicked(self, item, column):
        """
        Behandelt Doppelklick auf ein TreeWidget-Item, indem die Bearbeiten-Aktion ausgeführt wird.

        Args:
            item: Das angeklickte TreeWidgetItem
            column: Die Spalte des Doppelklicks
        """
        if item.isDisabled():
            return
        node_type = self._get_node_type_from_item(item)
        if node_type == ItemType.TREE_NODE:
            self._edit_tree_node(item)
        elif node_type == ItemType.XSL_FILE:
            self._edit_xsl_file(item)
        elif node_type == ItemType.XML_FILE:
            self._edit_xml_file(item)

    def _show_tree_context_menu(self, position):
        """
        Zeigt das Kontextmenü für das TreeWidget an.

        Args:
            position: Position des Rechtsklicks
        """
        # Hole das Item an der Position
        item = self.ui.treeWidget.itemAt(position)

        if not item:
            # Kein Item gefunden - zeige Kontextmenü für Root-Elemente
            node_type = ItemType.UNKNOWN
            context_menu = self._create_context_menu_for_type(node_type, None)
        else:
            # Bestimme den Node-Typ basierend auf dem Item
            node_type = self._get_node_type_from_item(item)
            # Erstelle das entsprechende Kontextmenü
            context_menu = self._create_context_menu_for_type(node_type, item)

        if context_menu:
            # Zeige das Kontextmenü an der globalen Position
            global_pos = self.ui.treeWidget.mapToGlobal(position)
            context_menu.exec(global_pos)

    def _on_tree_selection_changed(self):
        """
        Handler für Änderungen der Tree-Selektion.
        Lädt automatisch Diff-PDFs wenn ein XML-Knoten mit Diff-PDF ausgewählt wird.
        Leert den Viewer wenn ein Knoten ohne Diff-PDF ausgewählt wird.
        """
        try:
            logger.debug("Tree-Selektion geändert")

            # Hole aktuell selektierte Items
            selected_items = self.ui.treeWidget.selectedItems()

            if not selected_items or not self.project:
                # Keine Selektion oder kein Projekt - Viewer leeren
                logger.debug(
                    f"Keine Selektion oder kein Projekt: selected_items={len(selected_items) if selected_items else 0}, project={self.project is not None}"
                )
                if self.pdf_documents:
                    self._clear_pdf_viewer()
                return

            # Erstes selektiertes Item verwenden
            item = selected_items[0]

            # Prüfe ob es ein XML-Item ist
            node_type = self._get_node_type_from_item(item)
            logger.debug(f"Selektierter Node-Typ: {node_type}")

            if node_type == ItemType.XML_FILE:
                # Hole XmlFile-Objekt und XSL-ID aus UserRole
                xml_file_obj = item.data(0, Qt.ItemDataRole.UserRole)
                xsl_id_str = item.data(0, Qt.ItemDataRole.UserRole + 2)

                logger.debug(f"XML-File-Daten: xml_file_obj={xml_file_obj}, xsl_id_str={xsl_id_str}")

                if xml_file_obj and xsl_id_str:
                    # Extrahiere Pfad aus XmlFile-Objekt
                    xml_file_path = xml_file_obj.xml

                    # 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 = self.project.project_dir / "diff" / pdf_basename

                    logger.debug(f"Prüfe Diff-PDF: {diff_pdf_path}, existiert={diff_pdf_path.exists()}")

                    if diff_pdf_path.exists():
                        # Diff-PDF vorhanden - automatisch laden
                        logger.info(f"XML-Knoten mit Diff-PDF ausgewählt: {pdf_basename}, lade automatisch")
                        self._load_pdf_for_comparison(xml_file_path, xsl_id_str)
                    else:
                        # Kein Diff-PDF - Ref-PDF laden falls vorhanden, sonst Viewer leeren
                        ref_pdf_path = self.project.project_dir / "ref" / pdf_basename
                        if ref_pdf_path.exists():
                            logger.info(f"XML-Knoten ohne Diff-PDF, lade Ref-PDF: {pdf_basename}")
                            self._load_ref_pdf_for_display(xml_file_path, xsl_id_str)
                        elif self.pdf_documents:
                            logger.debug("XML-Knoten ohne Diff-PDF und ohne Ref-PDF, leere Viewer")
                            self._clear_pdf_viewer()
                else:
                    logger.debug("XML-File-Daten fehlen (xml_file_obj oder xsl_id_str ist None)")
            else:
                # Kein XML-Item - Viewer leeren falls noch ein PDF geladen ist
                if self.pdf_documents:
                    logger.debug(f"Nicht-XML-Knoten ausgewählt ({node_type}), leere Viewer")
                    self._clear_pdf_viewer()

        except Exception as e:
            logger.error(f"Fehler beim Verarbeiten der Tree-Selektion: {e}", exc_info=True)

    def _get_node_type_from_item(self, item):
        """
        Bestimmt den Node-Typ basierend auf dem TreeWidgetItem.

        Args:
            item: Das TreeWidgetItem

        Returns:
            str: Der Node-Typ ('TreeNode', 'XslFile', 'XmlFile' oder 'Unknown')
        """
        try:
            # Prüfe ob das Item ein Parent hat (dann ist es ein Child-Item)
            parent_item = item.parent()

            if parent_item:
                # Child-Item - prüfe ob es ein XML-File ist
                text = item.text(0)
                if text.startswith("XML:"):
                    return ItemType.XML_FILE
                else:
                    # Könnte ein TreeNode-Child oder XslFile-Child sein
                    # Prüfe den Parent-Typ
                    parent_type = self._get_node_type_from_item(parent_item)
                    if parent_type == ItemType.XSL_FILE:
                        return ItemType.XML_FILE
                    else:
                        # Rekursiv bestimmen basierend auf gespeicherten Daten
                        return self._determine_node_type_from_data(item)
            else:
                # Root-Item - bestimme Typ basierend auf gespeicherten Daten
                return self._determine_node_type_from_data(item)

        except Exception as e:
            logger.error(f"Fehler beim Bestimmen des Node-Typs: {e}")
            return ItemType.UNKNOWN

    def _determine_node_type_from_data(self, item):
        """
        Bestimmt den Node-Typ basierend auf den gespeicherten Daten im Item.

        Args:
            item: Das TreeWidgetItem

        Returns:
            str: Der Node-Typ ('TreeNode', 'XslFile' oder 'Unknown')
        """
        try:
            # Hole das gespeicherte Node-Objekt direkt
            node = item.data(0, Qt.ItemDataRole.UserRole)
            if not node:
                return ItemType.UNKNOWN

            # Bestimme den Typ direkt vom Node-Objekt
            if isinstance(node, TreeNode):
                return ItemType.TREE_NODE
            elif isinstance(node, XslFile):
                return ItemType.XSL_FILE
            elif isinstance(node, XmlFile):
                return ItemType.XML_FILE

            return ItemType.UNKNOWN

        except Exception as e:
            logger.error(f"Fehler beim Bestimmen des Node-Typs aus Daten: {e}")
            return ItemType.UNKNOWN

    def _find_item_by_node(self, node_obj):
        """
        Findet ein TreeWidgetItem basierend auf einem Node-Objekt.

        Args:
            node_obj: Das Node-Objekt (TreeNode, XslFile oder XmlFile)

        Returns:
            QTreeWidgetItem oder None wenn nicht gefunden
        """

        def search_recursive(item):
            """Rekursive Suche durch TreeWidget."""
            # Prüfe aktuelles Item
            item_node = item.data(0, Qt.ItemDataRole.UserRole)
            if item_node is node_obj:
                return item

            # Durchsuche Kinder
            for i in range(item.childCount()):
                child = item.child(i)
                result = search_recursive(child)
                if result:
                    return result
            return None

        # Durchsuche alle Root-Items
        for i in range(self.ui.treeWidget.topLevelItemCount()):
            root_item = self.ui.treeWidget.topLevelItem(i)
            result = search_recursive(root_item)
            if result:
                return result
        return None

    def _find_node_by_id(self, nodes, target_id):
        """
        Sucht rekursiv nach einem Node mit der angegebenen ID.

        Args:
            nodes: Liste der Nodes zum Durchsuchen
            target_id: Die zu suchende ID

        Returns:
            TreeNode|XslFile|None: Der gefundene Node oder None
        """
        for node in nodes:
            if node.id == target_id:
                return node

            # Rekursiv in Knotenn suchen (nur bei TreeNode)
            if isinstance(node, TreeNode) and node.children:
                found = self._find_node_by_id(node.children, target_id)
                if found:
                    return found

        return None

    def _create_context_menu_for_type(self, node_type, item):
        """
        Erstellt das Kontextmenü für den angegebenen Node-Typ.

        Args:
            node_type: Der Typ des Nodes ('TreeNode', 'XslFile', 'XmlFile')
            item: Das TreeWidgetItem

        Returns:
            QMenu: Das erstellte Kontextmenü oder None
        """
        try:
            menu = QMenu(self)

            if node_type == ItemType.TREE_NODE:
                # Kontextmenü für TreeNode
                action_edit = QAction("Bearbeiten", self)
                action_edit.setIcon(icon("settings"))
                action_edit.triggered.connect(lambda: self._edit_tree_node(item))
                font = action_edit.font()
                font.setBold(True)
                action_edit.setFont(font)
                menu.addAction(action_edit)
                menu.setDefaultAction(action_edit)

                menu.addSeparator()

                action_add_child = QAction("Unterknoten hinzufügen", self)
                action_add_child.setIcon(icon("folder-plus"))
                action_add_child.triggered.connect(lambda: self._add_tree_node_child(item))
                menu.addAction(action_add_child)

                action_add_xsl = QAction("XSL-Datei hinzufügen", self)
                action_add_xsl.setIcon(icon("file-plus"))
                action_add_xsl.triggered.connect(lambda: self._add_xsl_file_to_node(item))
                menu.addAction(action_add_xsl)

                menu.addSeparator()

                # Transformations-Aktionen (nur aktiv wenn XML-Dateien vorhanden)
                tree_node_obj = item.data(0, Qt.ItemDataRole.UserRole) if item else None
                has_xml_files = bool(tree_node_obj and self._has_xml_files_recursive(tree_node_obj))

                action_transform = QAction("Alle XML-Dateien transformieren", self)
                action_transform.setIcon(icon("play-circle"))
                action_transform.triggered.connect(lambda: self._transform_tree_node(item))
                action_transform.setEnabled(has_xml_files)
                menu.addAction(action_transform)

                action_transform_force = QAction("Alle XML-Dateien neu transformieren (force)", self)
                action_transform_force.setIcon(icon("refresh-cw"))
                action_transform_force.triggered.connect(lambda: self._transform_tree_node(item, force=True))
                action_transform_force.setEnabled(has_xml_files)
                menu.addAction(action_transform_force)

                menu.addSeparator()

                # Aktion "Alle Änderungen übernehmen" (nur aktiv wenn Diff-PDFs vorhanden)
                diff_pdfs = self._collect_all_diff_pdfs_under_node(tree_node_obj, item) if tree_node_obj else []
                has_diff_pdfs = len(diff_pdfs) > 0

                action_accept_all = QAction("Alle Änderungen übernehmen", self)
                action_accept_all.setIcon(icon("check-circle"))
                action_accept_all.triggered.connect(lambda: self._accept_all_changes_under_node(item))
                action_accept_all.setEnabled(has_diff_pdfs)
                menu.addAction(action_accept_all)

                menu.addSeparator()

                action_delete = QAction("Löschen", self)
                action_delete.setIcon(icon("trash-2"))
                action_delete.triggered.connect(lambda: self._delete_tree_node(item))
                menu.addAction(action_delete)

            elif node_type == ItemType.XSL_FILE:
                # Kontextmenü für XslFile
                action_edit = QAction("Bearbeiten", self)
                action_edit.setIcon(icon("settings"))
                action_edit.triggered.connect(lambda: self._edit_xsl_file(item))
                font = action_edit.font()
                font.setBold(True)
                action_edit.setFont(font)
                menu.addAction(action_edit)
                menu.setDefaultAction(action_edit)

                menu.addSeparator()

                action_add_xml = QAction("XML-Datei hinzufügen", self)
                action_add_xml.setIcon(icon("file-plus"))
                action_add_xml.triggered.connect(lambda: self._add_xml_file_to_xsl(item))
                menu.addAction(action_add_xml)

                menu.addSeparator()

                # Transformations-Aktionen (nur aktiv wenn XML-Dateien vorhanden)
                xsl_file_obj = item.data(0, Qt.ItemDataRole.UserRole) if item else None
                has_xml_files = bool(xsl_file_obj and xsl_file_obj.xmls)

                action_transform = QAction("Alle XML-Dateien transformieren", self)
                action_transform.setIcon(icon("play-circle"))
                action_transform.triggered.connect(lambda: self._transform_xsl_file(item))
                action_transform.setEnabled(has_xml_files)
                menu.addAction(action_transform)

                action_transform_force = QAction("Alle XML-Dateien neu transformieren (force)", self)
                action_transform_force.setIcon(icon("refresh-cw"))
                action_transform_force.triggered.connect(lambda: self._transform_xsl_file(item, force=True))
                action_transform_force.setEnabled(has_xml_files)
                menu.addAction(action_transform_force)

                menu.addSeparator()

                # Aktion "Alle Änderungen übernehmen" (nur aktiv wenn Diff-PDFs vorhanden)
                diff_pdfs = self._collect_all_diff_pdfs_under_node(xsl_file_obj, item) if xsl_file_obj else []
                has_diff_pdfs = len(diff_pdfs) > 0

                action_accept_all = QAction("Alle Änderungen übernehmen", self)
                action_accept_all.setIcon(icon("check-circle"))
                action_accept_all.triggered.connect(lambda: self._accept_all_changes_under_node(item))
                action_accept_all.setEnabled(has_diff_pdfs)
                menu.addAction(action_accept_all)

                menu.addSeparator()

                action_deps = QAction("Abhängigkeiten anzeigen", self)
                action_deps.setIcon(icon("git-branch"))
                action_deps.triggered.connect(lambda: self._show_xsl_dependencies(item))
                menu.addAction(action_deps)

                menu.addSeparator()

                action_delete = QAction("Löschen", self)
                action_delete.setIcon(icon("trash-2"))
                action_delete.triggered.connect(lambda: self._delete_xsl_file(item))
                menu.addAction(action_delete)

            elif node_type == ItemType.XML_FILE:
                # Kontextmenü für XmlFile
                action_edit = QAction("Bearbeiten", self)
                action_edit.setIcon(icon("settings"))
                action_edit.triggered.connect(lambda: self._edit_xml_file(item))
                font = action_edit.font()
                font.setBold(True)
                action_edit.setFont(font)
                menu.addAction(action_edit)
                menu.setDefaultAction(action_edit)

                menu.addSeparator()

                # Transformations-Aktionen
                action_transform = QAction("Transformieren", self)
                action_transform.setIcon(icon("play-circle"))
                action_transform.triggered.connect(lambda: self._transform_xml_file(item))
                menu.addAction(action_transform)

                action_transform_force = QAction("Neu transformieren (force)", self)
                action_transform_force.setIcon(icon("refresh-cw"))
                action_transform_force.triggered.connect(lambda: self._transform_xml_file(item, force=True))
                menu.addAction(action_transform_force)

                menu.addSeparator()

                action_delete = QAction("Löschen", self)
                action_delete.setIcon(icon("trash-2"))
                action_delete.triggered.connect(lambda: self._delete_xml_file(item))
                menu.addAction(action_delete)

            else:
                # Unbekannter Typ oder leerer Bereich - Menü für Root-Elemente
                action_add_tree_node = QAction("Unterknoten hinzufügen", self)
                action_add_tree_node.setIcon(icon("folder-plus"))
                action_add_tree_node.triggered.connect(lambda: self._add_root_tree_node())
                menu.addAction(action_add_tree_node)

            return menu

        except Exception as e:
            logger.error(f"Fehler beim Erstellen des Kontextmenüs: {e}")
            return None

    def _load_nodes_to_tree(self):
        """
        Lädt die Nodes aus den Projekt-Einstellungen in das TreeWidget.
        Sortiert die Items alphabetisch nach ihrer ID.
        """
        logger.info("Lade Nodes in TreeWidget...")

        try:
            # TreeWidget leeren
            self.ui.treeWidget.clear()

            # Lösche XML-Item-Map
            self.xml_item_map.clear()

            # Prüfe ob pdf_project existiert und Nodes hat
            if not hasattr(self, "pdf_project") or not self.pdf_project:
                logger.warning("Keine Projekt-Einstellungen verfügbar")
                return

            if not self.pdf_project.nodes:
                logger.warning("Keine Nodes in den Projekt-Einstellungen gefunden")
                return

            # Sortiere Root-Nodes alphabetisch nach ID
            sorted_nodes = sorted(self.pdf_project.nodes, key=lambda node: node.id)

            # Lade alle Root-Nodes (sortiert)
            for node in sorted_nodes:
                tree_item = self._create_tree_item_from_node(node)
                self.ui.treeWidget.addTopLevelItem(tree_item)

            logger.info(f"{len(self.pdf_project.nodes)} Root-Nodes in TreeWidget geladen (alphabetisch sortiert)")

            # Aktualisiere Diff-PDF-Anzahl und Icons nach dem Laden
            self._update_all_diff_pdf_counts()
            self._update_diff_icons_for_existing_pdfs()

            # Stelle Expand-Status wieder her
            self._restore_expanded_state()

            # Suchfilter erneut anwenden, falls aktiv
            search_text = self.ui.searchEdit.text()
            if search_text:
                self._filter_tree(search_text)

        except Exception as e:
            logger.error(f"Fehler beim Laden der Nodes in TreeWidget: {e}")

    def _filter_tree(self, text: str):
        """
        Filtert das TreeWidget nach TreeNode- und XslFile-Bezeichnungen sowie XSL-Dateinamen.

        Sichtbarkeitsregeln:
        - TreeNode/XslFile wird angezeigt, wenn .bez oder (bei XslFile) der Dateiname den Suchtext enthält (case-insensitive)
        - Wenn ein Kind matcht, bleibt der übergeordnete Knoten sichtbar und wird expandiert
        - Wenn ein TreeNode matcht, bleiben alle Kinder sichtbar
        - XmlFile-Items folgen der Sichtbarkeit ihres Eltern-XslFile
        - Leerer Suchtext blendet alles ein und stellt den Expand-Status wieder her

        Args:
            text: Suchtext für den Filter
        """
        search_lower = text.strip().lower()

        if not search_lower:
            # Alles einblenden, kollabieren und gespeicherten Expand-Status wiederherstellen
            for i in range(self.ui.treeWidget.topLevelItemCount()):
                item = self.ui.treeWidget.topLevelItem(i)
                self._reset_filter_recursive(item)
            self._restore_expanded_state()
            logger.debug("Baumfilter zurückgesetzt")
            return

        for i in range(self.ui.treeWidget.topLevelItemCount()):
            item = self.ui.treeWidget.topLevelItem(i)
            self._apply_filter_recursive(item, search_lower)

        logger.debug(f"Baumfilter angewendet: '{text}'")

    def _apply_filter_recursive(self, item: QTreeWidgetItem, search_lower: str) -> bool:
        """
        Wendet den Suchfilter rekursiv auf ein Item und seine Kinder an.

        Args:
            item: Das zu prüfende TreeWidgetItem
            search_lower: Suchtext in Kleinbuchstaben

        Returns:
            bool: True wenn dieses Item oder ein Kind den Suchtext enthält
        """
        node = item.data(0, Qt.ItemDataRole.UserRole)

        # Prüfe ob dieses Item selbst matcht (TreeNode/XslFile: .bez, XslFile zusätzlich: .xsl_file)
        self_matches = False
        if isinstance(node, (TreeNode, XslFile)):
            bez_text = str(node.bez).lower() if node.bez else ""
            self_matches = search_lower in bez_text
            if not self_matches and isinstance(node, XslFile):
                xsl_filename = node.xsl_file.name.lower() if node.xsl_file else ""
                self_matches = search_lower in xsl_filename

        # Wenn dieses Item matcht, sichtbar machen und alle Kinder einblenden
        if self_matches:
            item.setHidden(False)
            self._set_item_visible_recursive(item, True)
            item.setExpanded(True)
            return True

        # Prüfe Kinder rekursiv
        any_child_matches = False
        for child_idx in range(item.childCount()):
            child = item.child(child_idx)
            if self._apply_filter_recursive(child, search_lower):
                any_child_matches = True

        # Item sichtbar lassen wenn ein Kind matcht, expandieren für Übersicht
        if any_child_matches:
            item.setHidden(False)
            item.setExpanded(True)
        else:
            item.setHidden(True)

        return any_child_matches

    def _set_item_visible_recursive(self, item: QTreeWidgetItem, visible: bool):
        """
        Setzt die Sichtbarkeit eines Items und aller seiner Kinder.

        Args:
            item: Das TreeWidgetItem
            visible: Ob das Item sichtbar sein soll
        """
        item.setHidden(not visible)
        for child_idx in range(item.childCount()):
            self._set_item_visible_recursive(item.child(child_idx), visible)

    def _reset_filter_recursive(self, item: QTreeWidgetItem):
        """
        Blendet ein Item und alle Kinder ein und kollabiert sie.
        Kombiniert Sichtbarkeit und Expand-Reset in einem Durchlauf.

        Args:
            item: Das TreeWidgetItem
        """
        item.setHidden(False)
        item.setExpanded(False)
        for child_idx in range(item.childCount()):
            self._reset_filter_recursive(item.child(child_idx))

    def _create_tree_item_from_node(self, node):
        """
        Erstellt ein QTreeWidgetItem aus einem TreeNode oder XslFile.
        Speichert die vollständigen Node-Daten für spätere Verwendung.

        Args:
            node: TreeNode oder XslFile Objekt

        Returns:
            QTreeWidgetItem: Das erstellte Tree-Item mit vollständigen Node-Daten
        """
        try:
            # Erstelle Tree-Item
            item = QTreeWidgetItem()

            # Setze die Bezeichnung in Spalte 0
            bez_text = str(node.bez) if node.bez else ""
            item.setText(0, bez_text)

            # Speichere das komplette Node-Objekt als UserRole-Daten
            # Dies ermöglicht späteren Zugriff auf alle Node-Eigenschaften
            item.setData(0, Qt.ItemDataRole.UserRole, node)

            # Setze Icon basierend auf Node-Typ
            if isinstance(node, TreeNode):
                item.setIcon(0, icon("folder"))
            elif isinstance(node, XslFile):
                item.setIcon(0, icon("file-text"))

            if isinstance(node, TreeNode):
                # Speichere zusätzlich die Node-ID in UserRole+1 für Kompatibilität
                item.setData(0, Qt.ItemDataRole.UserRole + 1, node.id)

                # Lade Knoten rekursiv (sortiert nach ID)
                if node.children:
                    sorted_children = sorted(node.children, key=lambda child: child.id)
                    for child in sorted_children:
                        child_item = self._create_tree_item_from_node(child)
                        item.addChild(child_item)

                # Setze Diff-PDF-Anzahl in Spalte 1
                diff_count = self._count_diff_pdfs_under_node(node, item)
                if diff_count > 0:
                    item.setText(1, str(diff_count))

            elif isinstance(node, XslFile):
                # Speichere zusätzlich die Node-ID in UserRole+1 für Kompatibilität
                item.setData(0, Qt.ItemDataRole.UserRole + 1, node.id)

                # Setze Diff-PDF-Anzahl in Spalte 1
                diff_count = self._count_diff_pdfs_under_node(node, item)
                if diff_count > 0:
                    item.setText(1, str(diff_count))

                # Prüfe ob XSL-Datei existiert
                xsl_file_missing = False
                if hasattr(self, "project") and self.project:
                    from conf import app_settings

                    # Hole XSL-Verzeichnis aus Projekt-Konfiguration
                    xsl_dir = next((xd for xd in app_settings.xsl_dirs if xd.id == self.project.xsl_dir_id), None)

                    if xsl_dir:
                        # Konstruiere absoluten Pfad zur XSL-Datei
                        xsl_file_abs = xsl_dir.path_to_root_dir / node.xsl_file

                        if not xsl_file_abs.exists():
                            xsl_file_missing = True
                            item.setDisabled(True)
                            item.setToolTip(0, f"XSL-Datei nicht gefunden: {xsl_file_abs}")
                            logger.warning(f"XSL-Datei nicht vorhanden: {xsl_file_abs}")
                        else:
                            # Tooltip mit Abhängigkeiten (import/include) setzen
                            self._set_xsl_dependency_tooltip(item, xsl_file_abs)

                # Lade XML-Dateien als Knoten
                if node.xmls:
                    for xml in node.xmls:
                        xml_item = QTreeWidgetItem()
                        xml_item.setText(0, f"XML: {xml.xml.name}")

                        # Speichere auch das XmlFile-Objekt für XML-Items
                        xml_item.setData(0, Qt.ItemDataRole.UserRole, xml)
                        xml_item.setData(0, Qt.ItemDataRole.UserRole + 1, f"xml_{xml.xml.name}")

                        # Speichere XSL-ID in Spalte 0, UserRole+2 für einfachen Zugriff
                        xsl_id_str = "_".join(str(x) for x in node.id)
                        xml_item.setData(0, Qt.ItemDataRole.UserRole + 2, xsl_id_str)

                        # Setze XML-Icon
                        xml_item.setIcon(0, icon("code"))

                        # Prüfe ob XML-Datei existiert und deaktiviere Knoten falls nicht
                        # Wenn XSL-Datei fehlt, deaktiviere auch alle untergeordneten XML-Knoten
                        if xsl_file_missing:
                            xml_item.setDisabled(True)
                            xml_item.setToolTip(0, "XML-Knoten deaktiviert: Übergeordnete XSL-Datei fehlt")
                        elif hasattr(self, "project") and self.project:
                            xml_abs_path = self.project.project_dir / xml.xml
                            if not xml_abs_path.exists():
                                xml_item.setDisabled(True)
                                xml_item.setToolTip(0, f"XML-Datei nicht gefunden: {xml_abs_path}")
                                logger.warning(f"XML-Datei nicht vorhanden: {xml_abs_path}")

                        item.addChild(xml_item)

                        # Speichere XML-Item für spätere Widget-Updates (Progress Bar, Icon)
                        # Key: "xml_path|xsl_id" um mehrfache Verwendung derselben XML zu unterstützen
                        xml_path_str = str(xml.xml)
                        xsl_id_str = "_".join(str(x) for x in node.id)
                        map_key = f"{xml_path_str}|{xsl_id_str}"
                        self.xml_item_map[map_key] = xml_item
                        logger.debug(f"XML-Item zur Map hinzugefügt: '{map_key}'")

            return item

        except Exception as e:
            logger.error(f"Fehler beim Erstellen des Tree-Items: {e}")
            # Fallback: Erstelle einfaches Item
            fallback_item = QTreeWidgetItem()
            fallback_item.setText(0, "Fehler beim Laden")
            fallback_item.setToolTip(0, str(e))
            return fallback_item

    def _create_centered_progress_bar(self) -> tuple[QWidget, QProgressBar]:
        """
        Erstellt eine linksbündige Progress Bar in einem Container-Widget.

        Returns:
            tuple: (container_widget, progress_bar)
        """
        # Container-Widget erstellen
        container = QWidget()
        layout = QHBoxLayout(container)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setAlignment(Qt.AlignmentFlag.AlignLeft)

        # Progress Bar erstellen (indeterminate mode für pulsierenden Effekt)
        progress_bar = QProgressBar()
        progress_bar.setMinimum(0)
        progress_bar.setMaximum(0)  # Pulsierend
        progress_bar.setMaximumWidth(80)  # Kompakte Breite
        progress_bar.setMaximumHeight(16)  # Kompakte Höhe
        progress_bar.setTextVisible(False)

        layout.addWidget(progress_bar)

        return container, progress_bar

    def _create_centered_diff_icon(self, xml_file_path: Path, xsl_id_str: str) -> QWidget:
        """
        Erstellt ein linksbündiges, nicht-klickbares Icon für Diff-PDF.

        Args:
            xml_file_path: Pfad zur XML-Datei (relativ)
            xsl_id_str: XSL-ID als String (z.B. "2002_1_128")

        Returns:
            QWidget: Container mit Icon
        """
        # Container-Widget
        container = QWidget()
        layout = QHBoxLayout(container)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setAlignment(Qt.AlignmentFlag.AlignLeft)

        # Icon-Label
        icon_label = QLabel()
        icon_label.setPixmap(icon("columns").pixmap(16, 16))
        icon_label.setToolTip("Diff-PDF vorhanden (wird automatisch geladen bei Selektion)")

        layout.addWidget(icon_label)

        return container

    # Kontextmenü-Aktionen für TreeNode
    def _add_tree_node_child(self, parent_item):
        """Fügt einen Unterknoten zu einem TreeNode hinzu."""
        logger.debug(f"Unterknoten zu TreeNode hinzufügen: {parent_item.text(0)}")
        # TODO: Dialog zum Eingeben der Node-Daten öffnen

    def _add_xsl_file_to_node(self, parent_item):
        """Fügt eine XSL-Datei zu einem TreeNode hinzu."""
        logger.debug(f"XSL-Datei zu TreeNode hinzufügen: {parent_item.text(0)}")
        # TODO: Dialog zum Auswählen der XSL-Datei öffnen

    def _edit_tree_node(self, item):
        """
        Bearbeitet einen TreeNode.

        Args:
            item: Das TreeWidgetItem des TreeNode
        """
        logger.debug(f"TreeNode bearbeiten: {item.text(0)}")

        try:
            # Hole das Node-Objekt aus dem TreeWidgetItem
            node = item.data(0, Qt.ItemDataRole.UserRole)
            if not node or not isinstance(node, TreeNode):
                QMessageBox.warning(self, "Warnung", "Kein gültiger TreeNode gefunden.")
                return

            # Prüfe ob Projekt verfügbar ist
            if not self.pdf_project:
                QMessageBox.warning(self, "Warnung", "Keine Projekt-Einstellungen verfügbar.")
                return

            # Sammle Eltern-Parameter
            parent_params = self._collect_parent_params(item)

            # Erstelle und zeige den Dialog
            dialog = TreeNodeEditDialog(self, node, parent_params)
            if dialog.exec() == TreeNodeEditDialog.DialogCode.Accepted:
                # Hole die bearbeiteten Daten
                data = dialog.get_data()
                if data:
                    # Aktualisiere den Node
                    node.bez = data["bez"]
                    node.xslt_params = data["xslt_params"]

                    logger.info(f"TreeNode '{node.bez}' wurde aktualisiert")
                    logger.debug(f"XSLT-Parameter: {node.xslt_params}")

                    # Speichere die Änderungen
                    self._save_project_settings()

                    # Aktualisiere das TreeWidget
                    self._load_nodes_to_tree()

                    # Wenn Force-Transformation gewünscht, führe sie aus
                    if data.get("force_transform", False):
                        # Finde das neue Item nach dem Neuladen
                        new_item = self._find_item_by_node(node)
                        if new_item:
                            logger.info(f"Starte Force-Transformation für TreeNode '{node.bez}'")
                            self._transform_tree_node(new_item, force=True)
                        else:
                            logger.warning(f"Konnte Item für TreeNode '{node.bez}' nicht finden")

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

    def _collect_xsl_files_recursive(self, node):
        """
        Sammelt rekursiv alle XslFile-Objekte unter einem TreeNode.

        Args:
            node: Der TreeNode, dessen XslFiles gesammelt werden sollen

        Returns:
            list[XslFile]: Liste aller XslFiles im Teilbaum
        """
        xsl_files = []
        for child in node.children:
            if isinstance(child, XslFile):
                xsl_files.append(child)
            elif isinstance(child, TreeNode):
                xsl_files.extend(self._collect_xsl_files_recursive(child))
        return xsl_files

    def _count_sub_nodes_recursive(self, node):
        """
        Zählt rekursiv alle untergeordneten TreeNodes.

        Args:
            node: Der TreeNode, dessen Unterknoten gezählt werden sollen

        Returns:
            int: Anzahl der untergeordneten TreeNodes
        """
        count = 0
        for child in node.children:
            if isinstance(child, TreeNode):
                count += 1 + self._count_sub_nodes_recursive(child)
        return count

    def _delete_tree_node(self, item):
        """
        Löscht einen TreeNode und alle untergeordneten Elemente.

        Die XSL-Dateien werden nur aus dem Baum entfernt, nicht physisch gelöscht.
        Zugehörige PDF-Dateien werden automatisch bereinigt.
        XML-Dateien, die nirgends mehr verwendet werden, können optional physisch gelöscht werden.

        Args:
            item: Das TreeWidgetItem des TreeNodes
        """
        logger.debug(f"TreeNode löschen: {item.text(0)}")

        try:
            # Prüfe ob ein Projekt geladen ist
            if not hasattr(self, "project") or not self.project:
                QMessageBox.warning(self, "Warnung", "Kein Projekt geladen. Bitte öffnen Sie zuerst ein Projekt.")
                return

            if not hasattr(self, "pdf_project") or not self.pdf_project:
                QMessageBox.warning(self, "Warnung", "Keine Projekt-Einstellungen geladen.")
                return

            # Hole das TreeNode-Objekt aus dem TreeWidgetItem
            tree_node_obj = item.data(0, Qt.ItemDataRole.UserRole)
            if not tree_node_obj or not isinstance(tree_node_obj, TreeNode):
                QMessageBox.warning(self, "Warnung", "Kein gültiger Baumknoten gefunden.")
                return

            # Sammle alle XslFiles im Teilbaum vor der Löschung
            xsl_files = self._collect_xsl_files_recursive(tree_node_obj)
            sub_node_count = self._count_sub_nodes_recursive(tree_node_obj)

            # Bestätigungsdialog
            details = []
            if sub_node_count > 0:
                details.append(f"{sub_node_count} Unterknoten")
            if xsl_files:
                details.append(f"{len(xsl_files)} XSL-Datei(en)")
            detail_text = f"\n\nEnthaltene Elemente: {', '.join(details)}." if details else ""

            reply = QMessageBox.question(
                self,
                "Knoten löschen",
                f"Möchten Sie den Knoten '{tree_node_obj.bez}' und alle untergeordneten "
                f"Elemente aus dem Baum entfernen?\n\n"
                f"XSL-Dateien werden nicht physisch gelöscht.{detail_text}",
                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                QMessageBox.StandardButton.No,
            )

            if reply != QMessageBox.StandardButton.Yes:
                logger.debug("Löschung abgebrochen")
                return

            # Ermittle den Eltern-Kontext und entferne den TreeNode aus dem Datenmodell
            # WICHTIG: Zuerst entfernen, damit _is_xml_xsl_combination_used_elsewhere()
            # und _is_xml_file_used_elsewhere() den gelöschten Teilbaum nicht mehr sehen
            parent_item = item.parent()
            if parent_item:
                parent_node = parent_item.data(0, Qt.ItemDataRole.UserRole)
                if not parent_node or not isinstance(parent_node, TreeNode):
                    QMessageBox.warning(self, "Warnung", "Kein gültiger Eltern-Knoten gefunden.")
                    return
                children_before = len(parent_node.children)
                parent_node.children = [child for child in parent_node.children if child is not tree_node_obj]
                if len(parent_node.children) == children_before:
                    QMessageBox.warning(self, "Warnung", "Knoten konnte nicht aus dem Eltern-Knoten entfernt werden.")
                    return
            else:
                # Top-Level-Knoten
                nodes_before = len(self.pdf_project.nodes)
                self.pdf_project.nodes = [node for node in self.pdf_project.nodes if node is not tree_node_obj]
                if len(self.pdf_project.nodes) == nodes_before:
                    QMessageBox.warning(self, "Warnung", "Knoten konnte nicht aus dem Projekt entfernt werden.")
                    return

            # PDF-Bereinigung für alle gesammelten XslFiles
            total_deleted_pdfs = 0
            for xsl_file_obj in xsl_files:
                xsl_id = xsl_file_obj.id
                for xml_file_obj in xsl_file_obj.xmls:
                    is_combination_used = self._is_xml_xsl_combination_used_elsewhere(
                        xml_file_obj.xml, xsl_id, xsl_file_obj
                    )
                    if not is_combination_used:
                        deleted_pdfs = self._delete_pdf_files_for_xml_xsl_combination(xml_file_obj.xml, xsl_id)
                        total_deleted_pdfs += deleted_pdfs

            if total_deleted_pdfs > 0:
                logger.info(f"{total_deleted_pdfs} PDF-Datei(en) für Knoten '{tree_node_obj.bez}' gelöscht")

            # Sammle XML-Dateien, die nirgends mehr verwendet werden
            unused_xml_files = []
            seen_xml_paths = set()
            for xsl_file_obj in xsl_files:
                for xml_file_obj in xsl_file_obj.xmls:
                    xml_path_str = str(xml_file_obj.xml)
                    if xml_path_str in seen_xml_paths:
                        continue
                    seen_xml_paths.add(xml_path_str)
                    xml_file_path = Path(self.project.project_dir) / xml_file_obj.xml
                    if xml_file_path.exists():
                        is_used = self._is_xml_file_used_elsewhere(xml_file_obj.xml, xsl_file_obj)
                        if not is_used:
                            unused_xml_files.append((xml_file_obj, xml_file_path))

            # Nicht mehr verwendete XML-Dateien physisch löschen
            for xml_file_obj, xml_file_path in unused_xml_files:
                try:
                    xml_file_path.unlink()
                    logger.info(f"Physische XML-Datei gelöscht: {xml_file_path}")
                except Exception as e:
                    logger.warning(f"Konnte XML-Datei nicht löschen: {xml_file_path} - {e}")

            # Speichere und aktualisiere
            self._save_project_settings()
            self._load_nodes_to_tree()

            logger.info(f"TreeNode '{tree_node_obj.bez}' erfolgreich entfernt")

        except Exception as e:
            error_msg = f"Fehler beim Löschen des Knotens: {str(e)}"
            logger.error(error_msg)
            QMessageBox.critical(self, "Fehler", error_msg)

    # Kontextmenü-Aktionen für XslFile
    def _add_xml_file_to_xsl(self, parent_item):
        """
        Fügt eine XML-Datei zu einer XSL-Datei hinzu.

        Args:
            parent_item: Das TreeWidgetItem des XslFile-Nodes
        """
        logger.debug(f"XML-Datei zu XslFile hinzufügen: {parent_item.text(0)}")

        try:
            # Prüfe ob ein Projekt geladen ist
            if not hasattr(self, "project") or not self.project:
                QMessageBox.warning(self, "Warnung", "Kein Projekt geladen. Bitte öffnen Sie zuerst ein Projekt.")
                return

            if not hasattr(self, "pdf_project") or not self.pdf_project:
                QMessageBox.warning(self, "Warnung", "Keine Projekt-Einstellungen geladen.")
                return

            # Hole das XslFile-Node-Objekt direkt aus dem TreeWidgetItem
            xsl_node = parent_item.data(0, Qt.ItemDataRole.UserRole)
            if not xsl_node or not isinstance(xsl_node, XslFile):
                QMessageBox.warning(self, "Warnung", "Keine gültige XSL-Datei-Node gefunden.")
                return

            # Öffne Datei-Dialog zum Auswählen der XML-Datei
            xml_file_path, _ = QFileDialog.getOpenFileName(
                self, "XML-Datei auswählen", "", "XML-Dateien (*.xml);;Alle Dateien (*)"
            )

            if not xml_file_path:
                # Benutzer hat abgebrochen
                return

            xml_file_path = Path(xml_file_path)

            # Prüfe ob die Datei existiert
            if not xml_file_path.exists():
                QMessageBox.critical(self, "Fehler", f"Die ausgewählte XML-Datei existiert nicht:\n{xml_file_path}")
                return

            # Erstelle xml-Ordner im Projekt-Verzeichnis falls er nicht existiert
            xml_dir = Path(self.project.project_dir) / "xml"
            xml_dir.mkdir(parents=True, exist_ok=True)

            # Bestimme den Ziel-Pfad in xml-Ordner
            target_xml_path = xml_dir / xml_file_path.name

            # Prüfe ob eine Datei mit gleichem Namen bereits existiert
            if target_xml_path.exists():
                reply = QMessageBox.question(
                    self,
                    "Datei existiert bereits",
                    f"Eine XML-Datei mit dem Namen '{xml_file_path.name}' existiert bereits im xml-Ordner.\n\n"
                    "Möchten Sie sie überschreiben?",
                    QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                    QMessageBox.StandardButton.No,
                )

                if reply != QMessageBox.StandardButton.Yes:
                    return

            # Kopiere die XML-Datei in den xml-Ordner
            shutil.copy2(xml_file_path, target_xml_path)
            logger.info(f"XML-Datei kopiert: {xml_file_path} -> {target_xml_path}")

            # Erstelle relatives Path zur XML-Datei (relativ zum xml-Ordner)
            relative_xml_path = Path("xml") / xml_file_path.name

            # Prüfe ob diese XML-Datei bereits in der XslFile-Node vorhanden ist
            existing_xml = None
            for xml_file in xsl_node.xmls:
                if xml_file.xml == relative_xml_path:
                    existing_xml = xml_file
                    break

            if existing_xml:
                QMessageBox.information(
                    self,
                    "XML-Datei bereits vorhanden",
                    f"Die XML-Datei '{xml_file_path.name}' ist bereits in dieser XSL-Datei enthalten.",
                )
                return

            # Erstelle neues XmlFile-Objekt und füge es zur XslFile-Node hinzu
            new_xml_file = XmlFile(xml=relative_xml_path)
            xsl_node.xmls.append(new_xml_file)

            logger.info(f"XML-Datei '{xml_file_path.name}' zu XslFile-Node '{xsl_node.bez}' hinzugefügt")

            # Berechne Hash für die neue XML-Datei
            self._calculate_hash_for_xml_file(new_xml_file)

            # Speichere die aktualisierten Projekt-Einstellungen
            self._save_project_settings()

            # Aktualisiere das TreeWidget
            self._load_nodes_to_tree()

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

    def _edit_xsl_file(self, item):
        """
        Bearbeitet eine XSL-Datei.

        Args:
            item: Das TreeWidgetItem des XslFile
        """
        logger.debug(f"XslFile bearbeiten: {item.text(0)}")

        try:
            # Hole das Node-Objekt aus dem TreeWidgetItem
            node = item.data(0, Qt.ItemDataRole.UserRole)
            if not node or not isinstance(node, XslFile):
                QMessageBox.warning(self, "Warnung", "Keine gültige XSL-Datei gefunden.")
                return

            # Sammle Eltern-Parameter
            parent_params = self._collect_parent_params(item)

            # Erstelle und zeige den Dialog
            dialog = XslFileEditDialog(self, node, parent_params)
            if dialog.exec() == XslFileEditDialog.DialogCode.Accepted:
                # Hole die bearbeiteten Daten
                data = dialog.get_data()
                if data:
                    # Aktualisiere den Node
                    node.bez = data["bez"]
                    node.xslt_params = data["xslt_params"]

                    logger.info(f"XslFile '{node.bez}' wurde aktualisiert")
                    logger.debug(f"XSLT-Parameter: {node.xslt_params}")

                    # Speichere die Änderungen
                    self._save_project_settings()

                    # Aktualisiere das TreeWidget
                    self._load_nodes_to_tree()

                    # Wenn Force-Transformation gewünscht, führe sie aus
                    if data.get("force_transform", False):
                        # Finde das neue Item nach dem Neuladen
                        new_item = self._find_item_by_node(node)
                        if new_item:
                            logger.info(f"Starte Force-Transformation für XslFile '{node.bez}'")
                            self._transform_xsl_file(new_item, force=True)
                        else:
                            logger.warning(f"Konnte Item für XslFile '{node.bez}' nicht finden")

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

    def _delete_xsl_file(self, item):
        """
        Löscht eine XSL-Datei aus einem Baumknoten.

        Die XSL-Datei wird nur aus dem Baum entfernt, nicht physisch gelöscht.
        Zugehörige PDF-Dateien werden automatisch bereinigt.
        XML-Dateien, die nirgends mehr verwendet werden, können optional physisch gelöscht werden.

        Args:
            item: Das TreeWidgetItem der XSL-Datei
        """
        logger.debug(f"XslFile löschen: {item.text(0)}")

        try:
            # Prüfe ob ein Projekt geladen ist
            if not hasattr(self, "project") or not self.project:
                QMessageBox.warning(self, "Warnung", "Kein Projekt geladen. Bitte öffnen Sie zuerst ein Projekt.")
                return

            if not hasattr(self, "pdf_project") or not self.pdf_project:
                QMessageBox.warning(self, "Warnung", "Keine Projekt-Einstellungen geladen.")
                return

            # Hole das XslFile-Objekt aus dem TreeWidgetItem
            xsl_file_obj = item.data(0, Qt.ItemDataRole.UserRole)
            if not xsl_file_obj or not isinstance(xsl_file_obj, XslFile):
                QMessageBox.warning(self, "Warnung", "Keine gültige XSL-Datei gefunden.")
                return

            # Hole das Eltern-Item (sollte ein TreeNode sein)
            parent_item = item.parent()
            if not parent_item:
                QMessageBox.warning(self, "Warnung", "Eltern-Knoten nicht gefunden.")
                return

            parent_node = parent_item.data(0, Qt.ItemDataRole.UserRole)
            if not parent_node or not isinstance(parent_node, TreeNode):
                QMessageBox.warning(self, "Warnung", "Kein gültiger Eltern-Knoten gefunden.")
                return

            # Bestätigungsdialog anzeigen
            xml_count = len(xsl_file_obj.xmls)
            xml_info = f"\n\nDer XSL-Datei sind {xml_count} XML-Datei(en) zugeordnet." if xml_count > 0 else ""
            reply = QMessageBox.question(
                self,
                "XSL-Datei entfernen",
                f"Möchten Sie die XSL-Datei '{xsl_file_obj.bez}' aus dem Baum entfernen?\n\n"
                f"Die XSL-Datei selbst wird nicht physisch gelöscht.{xml_info}",
                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                QMessageBox.StandardButton.No,
            )

            if reply != QMessageBox.StandardButton.Yes:
                logger.debug("Löschung abgebrochen")
                return

            # PDF-Bereinigung für alle zugehörigen XML-Dateien
            xsl_id = xsl_file_obj.id
            total_deleted_pdfs = 0
            for xml_file_obj in xsl_file_obj.xmls:
                is_combination_used = self._is_xml_xsl_combination_used_elsewhere(
                    xml_file_obj.xml, xsl_id, xsl_file_obj
                )
                if not is_combination_used:
                    deleted_pdfs = self._delete_pdf_files_for_xml_xsl_combination(xml_file_obj.xml, xsl_id)
                    total_deleted_pdfs += deleted_pdfs

            if total_deleted_pdfs > 0:
                logger.info(f"{total_deleted_pdfs} PDF-Datei(en) für XSL '{xsl_file_obj.bez}' gelöscht")

            # Sammle XML-Dateien, die nirgends mehr verwendet werden
            unused_xml_files = []
            for xml_file_obj in xsl_file_obj.xmls:
                xml_file_path = Path(self.project.project_dir) / xml_file_obj.xml
                if xml_file_path.exists():
                    is_used = self._is_xml_file_used_elsewhere(xml_file_obj.xml, xsl_file_obj)
                    if not is_used:
                        unused_xml_files.append((xml_file_obj, xml_file_path))

            # Optionale physische Löschung nicht mehr verwendeter XML-Dateien
            if unused_xml_files:
                file_list = "\n".join(f"  - {p.name}" for _, p in unused_xml_files)
                delete_reply = QMessageBox.question(
                    self,
                    "Physische XML-Dateien löschen",
                    f"Folgende {len(unused_xml_files)} XML-Datei(en) werden in keiner anderen "
                    f"XSL-Datei mehr verwendet:\n\n{file_list}\n\n"
                    "Möchten Sie diese physisch löschen?",
                    QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                    QMessageBox.StandardButton.No,
                )

                if delete_reply == QMessageBox.StandardButton.Yes:
                    for xml_file_obj, xml_file_path in unused_xml_files:
                        try:
                            xml_file_path.unlink()
                            logger.info(f"Physische XML-Datei gelöscht: {xml_file_path}")
                        except Exception as e:
                            logger.warning(f"Konnte XML-Datei nicht löschen: {xml_file_path} - {e}")

            # Entferne das XslFile aus dem Eltern-TreeNode
            children_before = len(parent_node.children)
            parent_node.children = [child for child in parent_node.children if child is not xsl_file_obj]
            children_after = len(parent_node.children)

            if children_before == children_after:
                QMessageBox.warning(self, "Warnung", "XSL-Datei konnte nicht aus dem Knoten entfernt werden.")
                return

            logger.info(f"XSL-Datei '{xsl_file_obj.bez}' aus Knoten '{parent_node.bez}' entfernt")

            # Speichere und aktualisiere
            self._save_project_settings()
            self._load_nodes_to_tree()

            logger.info(f"XSL-Datei '{xsl_file_obj.bez}' erfolgreich entfernt")

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

    def _get_xsl_abs_path(self, xsl_file_obj: XslFile) -> Path | None:
        """
        Ermittelt den absoluten Pfad einer XSL-Datei anhand der Projekt-Konfiguration.

        Args:
            xsl_file_obj: Das XslFile-Objekt

        Returns:
            Absoluter Pfad oder None wenn nicht ermittelbar
        """
        if not hasattr(self, "project") or not self.project:
            return None

        from conf import app_settings

        xsl_dir = next((xd for xd in app_settings.xsl_dirs if xd.id == self.project.xsl_dir_id), None)
        if not xsl_dir:
            return None

        return xsl_dir.path_to_root_dir / xsl_file_obj.xsl_file

    def _ensure_xsl_dependency_graph(self):
        """Stellt sicher, dass der XSL-Abhängigkeitsgraph initialisiert ist."""
        if not hasattr(self, "xsl_dependency_graph") or self.xsl_dependency_graph is None:
            from xsl_dependencies import XslDependencyGraph

            self.xsl_dependency_graph = XslDependencyGraph()

    def _set_xsl_dependency_tooltip(self, item: QTreeWidgetItem, xsl_file_abs: Path):
        """
        Setzt einen Tooltip mit den Abhängigkeiten (import/include) einer XSL-Datei.

        Args:
            item: Das TreeWidgetItem der XSL-Datei
            xsl_file_abs: Absoluter Pfad zur XSL-Datei
        """
        self._ensure_xsl_dependency_graph()

        deps = self.xsl_dependency_graph.get_dependencies(xsl_file_abs)
        if not deps:
            item.setToolTip(0, f"{xsl_file_abs.name}\nKeine Abhängigkeiten (import/include)")
            return

        # Sortierte Liste der Abhängigkeiten (nur Dateinamen)
        dep_names = sorted(dep.name for dep in deps)
        dep_list = "\n".join(f"  - {name}" for name in dep_names)
        tooltip = f"{xsl_file_abs.name}\n{len(deps)} Abhängigkeit(en):\n{dep_list}"
        item.setToolTip(0, tooltip)

    def _show_xsl_dependencies(self, item: QTreeWidgetItem):
        """
        Zeigt einen Dialog mit den Abhängigkeiten einer XSL-Datei.

        Args:
            item: Das TreeWidgetItem der XSL-Datei
        """
        xsl_file_obj = item.data(0, Qt.ItemDataRole.UserRole)
        if not isinstance(xsl_file_obj, XslFile):
            return

        xsl_file_abs = self._get_xsl_abs_path(xsl_file_obj)
        if not xsl_file_abs or not xsl_file_abs.exists():
            QMessageBox.warning(self, "Fehler", f"XSL-Datei nicht gefunden: {xsl_file_abs}")
            return

        self._ensure_xsl_dependency_graph()
        deps = self.xsl_dependency_graph.get_dependencies(xsl_file_abs)

        if not deps:
            QMessageBox.information(
                self,
                f"Abhängigkeiten: {xsl_file_obj.bez}",
                f"Die XSL-Datei '{xsl_file_abs.name}' hat keine Abhängigkeiten (import/include).",
            )
            return

        # Sortierte Liste mit relativen Pfaden (relativ zum XSL-Verzeichnis)
        xsl_root = xsl_file_abs.parent
        from conf import app_settings

        xsl_dir = next((xd for xd in app_settings.xsl_dirs if xd.id == self.project.xsl_dir_id), None)
        if xsl_dir:
            xsl_root = xsl_dir.path_to_root_dir

        dep_lines = []
        for dep in sorted(deps, key=lambda p: p.name):
            try:
                rel_path = dep.relative_to(xsl_root)
            except ValueError:
                rel_path = dep.name
            dep_lines.append(f"  - {rel_path}")

        dep_text = "\n".join(dep_lines)
        QMessageBox.information(
            self,
            f"Abhängigkeiten: {xsl_file_obj.bez}",
            f"Die XSL-Datei '{xsl_file_abs.name}' importiert/inkludiert {len(deps)} Datei(en):\n\n{dep_text}",
        )

    # Kontextmenü-Aktionen für XmlFile
    def _edit_xml_file(self, item):
        """
        Bearbeitet die Zuordnungen einer XML-Datei zu XSL-Knoten.
        Öffnet den XmlToXslAssignDialog im Edit-Modus mit Vorauswahl aller bestehenden Zuordnungen.
        """
        logger.debug(f"XmlFile bearbeiten: {item.text(0)}")

        try:
            if not hasattr(self, "project") or not self.project:
                QMessageBox.warning(self, "Warnung", "Kein Projekt geladen. Bitte öffnen Sie zuerst ein Projekt.")
                return

            if not hasattr(self, "pdf_project") or not self.pdf_project:
                QMessageBox.warning(self, "Warnung", "Keine Projekt-Einstellungen geladen.")
                return

            xml_file_obj = item.data(0, Qt.ItemDataRole.UserRole)
            if not xml_file_obj or not isinstance(xml_file_obj, XmlFile):
                QMessageBox.warning(self, "Warnung", "Keine gültige XML-Datei gefunden.")
                return

            xml_path = xml_file_obj.xml
            # Alle aktuellen XSL-IDs, an denen diese XML hängt
            current_xsl_ids = self._find_xsl_ids_for_xml(xml_path)
            # Hash von einer vorhandenen Instanz übernehmen (falls vorhanden)
            existing_hash = self._find_existing_xml_hash(xml_path)

            dialog = XmlToXslAssignDialog(
                parent=self,
                xml_file_path=xml_path,
                project_nodes=self.pdf_project.nodes,
                preselected_xsl_ids=current_xsl_ids,
                edit_mode=True,
            )

            if dialog.exec() != XmlToXslAssignDialog.DialogCode.Accepted:
                logger.debug("Bearbeiten abgebrochen")
                return

            added_nodes, removed_nodes = dialog.get_selection_diff()

            if not added_nodes and not removed_nodes:
                logger.debug("Keine Änderungen an den XML-Zuordnungen")
                return

            self._apply_xml_assignment_changes(xml_path, existing_hash, added_nodes, removed_nodes)

            # Speichern und Baum neu laden
            self._save_project_settings()
            self._load_nodes_to_tree()

            # Wenn keine Zuordnungen mehr bestehen: Physische Datei löschen anbieten
            if not self._find_xsl_ids_for_xml(xml_path):
                xml_file_path = Path(self.project.project_dir) / xml_path
                if xml_file_path.exists():
                    reply = QMessageBox.question(
                        self,
                        "Physische Datei löschen",
                        f"Die XML-Datei '{xml_path.name}' ist keinem XSL-Knoten mehr zugeordnet.\n\n"
                        "Möchten Sie auch die physische Datei aus dem xml-Ordner löschen?",
                        QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                        QMessageBox.StandardButton.No,
                    )
                    if reply == QMessageBox.StandardButton.Yes:
                        try:
                            xml_file_path.unlink()
                            logger.info(f"Physische XML-Datei gelöscht: {xml_file_path}")
                        except Exception as e:
                            QMessageBox.warning(self, "Warnung", f"Fehler beim Löschen der physischen Datei:\n{str(e)}")

            logger.info(
                f"XML-Zuordnungen aktualisiert: {len(added_nodes)} hinzugefügt, {len(removed_nodes)} entfernt"
            )

        except Exception as e:
            error_msg = f"Fehler beim Bearbeiten der XML-Zuordnungen: {str(e)}"
            logger.exception(error_msg)
            QMessageBox.critical(self, "Fehler", error_msg)

    def _find_xsl_ids_for_xml(self, xml_path: Path) -> set:
        """
        Findet alle XslFile-IDs (tuple), deren `xmls`-Liste den angegebenen Pfad enthält.

        Args:
            xml_path: Relativer Pfad zur XML-Datei

        Returns:
            set[tuple]: Set von XslFile-IDs
        """
        found_ids: set = set()
        if not self.pdf_project or not self.pdf_project.nodes:
            return found_ids

        def _walk(nodes):
            for node in nodes:
                if isinstance(node, XslFile):
                    if any(xml.xml == xml_path for xml in node.xmls):
                        found_ids.add(node.id)
                elif isinstance(node, TreeNode) and node.children:
                    _walk(node.children)

        _walk(self.pdf_project.nodes)
        return found_ids

    def _find_existing_xml_hash(self, xml_path: Path) -> str | None:
        """
        Sucht den blake2b-Hash einer bereits zugeordneten XmlFile-Instanz.
        Gibt den ersten gefundenen Hash zurück (oder None, wenn keiner vorhanden).
        """
        if not self.pdf_project or not self.pdf_project.nodes:
            return None

        def _walk(nodes):
            for node in nodes:
                if isinstance(node, XslFile):
                    for xml_file in node.xmls:
                        if xml_file.xml == xml_path and xml_file.hashsum:
                            return xml_file.hashsum
                elif isinstance(node, TreeNode) and node.children:
                    found = _walk(node.children)
                    if found:
                        return found
            return None

        return _walk(self.pdf_project.nodes)

    def _apply_xml_assignment_changes(
        self, xml_path: Path, hashsum: str | None, added_nodes: list, removed_nodes: list
    ):
        """
        Wendet Änderungen an den XML-Zuordnungen an:
        - Entfernt XmlFile-Instanzen aus `removed_nodes` und löscht zugehörige PDFs (falls Kombination nirgends mehr existiert)
        - Fügt XmlFile-Instanzen zu `added_nodes` hinzu (mit übernommenem Hash)

        Args:
            xml_path: Relativer Pfad zur XML-Datei
            hashsum: blake2b-Hash, der für neue Instanzen übernommen wird
            added_nodes: Liste der XslFile-Objekte, denen die XML neu zugeordnet wird
            removed_nodes: Liste der XslFile-Objekte, aus denen die XML entfernt wird
        """
        # 1. Entfernen
        for xsl_node in removed_nodes:
            xsl_node.xmls = [x for x in xsl_node.xmls if x.xml != xml_path]

            # PDFs nur löschen, wenn die Kombination nirgends mehr existiert
            if not self._is_xml_xsl_combination_used_elsewhere(xml_path, xsl_node.id, xsl_node):
                deleted = self._delete_pdf_files_for_xml_xsl_combination(xml_path, xsl_node.id)
                if deleted:
                    logger.info(f"{deleted} PDF-Datei(en) für '{xml_path.name}' (XSL: {xsl_node.bez}) gelöscht")

        # 2. Hinzufügen (Hash übernehmen, Duplikate im gleichen XslFile vermeiden)
        for xsl_node in added_nodes:
            if any(x.xml == xml_path for x in xsl_node.xmls):
                logger.debug(f"XML '{xml_path.name}' bereits an XSL '{xsl_node.bez}' zugeordnet — übersprungen")
                continue
            xsl_node.xmls.append(XmlFile(xml=xml_path, hashsum=hashsum))
            logger.info(f"XML '{xml_path.name}' zu XSL '{xsl_node.bez}' hinzugefügt")

    def _delete_xml_file(self, item):
        """
        Löscht eine XML-Datei aus einem XSL-Knoten.

        Args:
            item: Das TreeWidgetItem der XML-Datei
        """
        logger.debug(f"XmlFile löschen: {item.text(0)}")

        try:
            # Prüfe ob ein Projekt geladen ist
            if not hasattr(self, "project") or not self.project:
                QMessageBox.warning(self, "Warnung", "Kein Projekt geladen. Bitte öffnen Sie zuerst ein Projekt.")
                return

            if not hasattr(self, "pdf_project") or not self.pdf_project:
                QMessageBox.warning(self, "Warnung", "Keine Projekt-Einstellungen geladen.")
                return

            # Hole das XmlFile-Objekt aus dem TreeWidgetItem
            xml_file_obj = item.data(0, Qt.ItemDataRole.UserRole)
            if not xml_file_obj or not isinstance(xml_file_obj, XmlFile):
                QMessageBox.warning(self, "Warnung", "Keine gültige XML-Datei gefunden.")
                return

            # Hole das Eltern-Item (sollte ein XslFile sein)
            parent_item = item.parent()
            if not parent_item:
                QMessageBox.warning(self, "Warnung", "Eltern-XSL-Datei nicht gefunden.")
                return

            # Hole das XslFile-Objekt aus dem Eltern-Item
            xsl_file_obj = parent_item.data(0, Qt.ItemDataRole.UserRole)
            if not xsl_file_obj or not isinstance(xsl_file_obj, XslFile):
                QMessageBox.warning(self, "Warnung", "Keine gültige Eltern-XSL-Datei gefunden.")
                return

            # Bestätigungsdialog anzeigen
            xml_filename = xml_file_obj.xml.name
            reply = QMessageBox.question(
                self,
                "XML-Datei löschen",
                f"Möchten Sie die XML-Datei '{xml_filename}' aus der XSL-Datei '{xsl_file_obj.bez}' entfernen?\n\n"
                "Die XML-Datei wird nur aus der Zuordnung entfernt, nicht physisch gelöscht.",
                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                QMessageBox.StandardButton.No,
            )

            if reply != QMessageBox.StandardButton.Yes:
                logger.debug("Löschung abgebrochen")
                return

            # Entferne die XML-Datei aus der XslFile-Node
            xml_files_before = len(xsl_file_obj.xmls)
            xsl_file_obj.xmls = [xml for xml in xsl_file_obj.xmls if xml.xml != xml_file_obj.xml]
            xml_files_after = len(xsl_file_obj.xmls)

            if xml_files_before == xml_files_after:
                QMessageBox.warning(self, "Warnung", "XML-Datei konnte nicht aus der XSL-Datei entfernt werden.")
                return

            logger.info(f"XML-Datei '{xml_filename}' aus XSL-Datei '{xsl_file_obj.bez}' entfernt")

            # Prüfe ob die XML+XSL-Kombination noch woanders verwendet wird
            # Wenn nicht, lösche die zugehörigen PDF-Dateien
            xsl_id = xsl_file_obj.id
            is_combination_used_elsewhere = self._is_xml_xsl_combination_used_elsewhere(
                xml_file_obj.xml, xsl_id, xsl_file_obj
            )

            if not is_combination_used_elsewhere:
                # Lösche alle PDF-Dateien für diese XML+XSL-Kombination
                deleted_pdfs = self._delete_pdf_files_for_xml_xsl_combination(xml_file_obj.xml, xsl_id)
                if deleted_pdfs > 0:
                    logger.info(f"{deleted_pdfs} PDF-Datei(en) für '{xml_filename}' (XSL: {xsl_file_obj.bez}) gelöscht")
            else:
                logger.debug(
                    f"XML+XSL-Kombination '{xml_filename}' + XSL-ID {xsl_id} wird noch anderswo verwendet - PDFs nicht gelöscht"
                )

            # Frage ob die physische Datei auch gelöscht werden soll
            xml_file_path = Path(self.project.project_dir) / xml_file_obj.xml
            if xml_file_path.exists():
                # Prüfe ob die XML-Datei noch in anderen XSL-Dateien verwendet wird
                is_used_elsewhere = self._is_xml_file_used_elsewhere(xml_file_obj.xml, xsl_file_obj)

                if not is_used_elsewhere:
                    delete_reply = QMessageBox.question(
                        self,
                        "Physische Datei löschen",
                        f"Die XML-Datei '{xml_filename}' wird in keiner anderen XSL-Datei verwendet.\n\n"
                        "Möchten Sie auch die physische Datei aus dem xml-Ordner löschen?",
                        QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                        QMessageBox.StandardButton.No,
                    )

                    if delete_reply == QMessageBox.StandardButton.Yes:
                        try:
                            xml_file_path.unlink()
                            logger.info(f"Physische XML-Datei gelöscht: {xml_file_path}")
                        except Exception as e:
                            QMessageBox.warning(self, "Warnung", f"Fehler beim Löschen der physischen Datei:\n{str(e)}")
                else:
                    logger.info(
                        f"XML-Datei '{xml_filename}' wird noch in anderen XSL-Dateien verwendet - physische Datei nicht gelöscht"
                    )

            # Speichere die aktualisierten Projekt-Einstellungen
            self._save_project_settings()

            # Aktualisiere das TreeWidget
            self._load_nodes_to_tree()

            logger.info(f"XML-Datei '{xml_filename}' erfolgreich entfernt")

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

    def _is_xml_file_used_elsewhere(self, xml_path, exclude_xsl_file):
        """
        Prüft ob eine XML-Datei noch in anderen XSL-Dateien verwendet wird.

        Args:
            xml_path: Pfad zur XML-Datei (relativ)
            exclude_xsl_file: XSL-Datei die ausgeschlossen werden soll

        Returns:
            bool: True wenn die XML-Datei noch anderswo verwendet wird
        """
        try:
            # Prüfe ob pdf_project und nodes existieren
            if not self.pdf_project or not self.pdf_project.nodes:
                return False  # Keine Nodes vorhanden, also nicht verwendet

            return self._check_xml_usage_recursive(self.pdf_project.nodes, xml_path, exclude_xsl_file)
        except Exception as e:
            logger.error(f"Fehler beim Prüfen der XML-Datei-Verwendung: {e}")
            return True  # Im Zweifelsfall annehmen, dass sie verwendet wird

    def _check_xml_usage_recursive(self, nodes, xml_path, exclude_xsl_file):
        """
        Prüft rekursiv ob eine XML-Datei in den Nodes verwendet wird.

        Args:
            nodes: Liste der zu prüfenden Nodes
            xml_path: Pfad zur XML-Datei (relativ)
            exclude_xsl_file: XSL-Datei die ausgeschlossen werden soll

        Returns:
            bool: True wenn die XML-Datei gefunden wird
        """
        for node in nodes:
            if isinstance(node, XslFile) and node != exclude_xsl_file:
                # Prüfe ob diese XSL-Datei die XML-Datei verwendet
                for xml_file in node.xmls:
                    if xml_file.xml == xml_path:
                        return True
            elif isinstance(node, TreeNode) and node.children:
                # Rekursiv in Knoten suchen
                if self._check_xml_usage_recursive(node.children, xml_path, exclude_xsl_file):
                    return True

        return False

    def _is_xml_xsl_combination_used_elsewhere(self, xml_path: Path, xsl_id: tuple, exclude_xsl_file) -> bool:
        """
        Prüft ob eine XML+XSL-Kombination noch in anderen XSL-Dateien verwendet wird.

        Args:
            xml_path: Pfad zur XML-Datei (relativ)
            xsl_id: Tuple der XSL-ID
            exclude_xsl_file: XSL-Datei die ausgeschlossen werden soll

        Returns:
            bool: True wenn die Kombination noch anderswo verwendet wird
        """
        try:
            if not self.pdf_project or not self.pdf_project.nodes:
                return False

            return self._check_xml_xsl_combination_recursive(self.pdf_project.nodes, xml_path, xsl_id, exclude_xsl_file)
        except Exception as e:
            logger.error(f"Fehler beim Prüfen der XML+XSL-Kombination: {e}")
            return True  # Im Zweifelsfall annehmen, dass sie verwendet wird

    def _check_xml_xsl_combination_recursive(self, nodes, xml_path: Path, xsl_id: tuple, exclude_xsl_file) -> bool:
        """
        Prüft rekursiv ob eine XML+XSL-Kombination in den Nodes verwendet wird.

        Args:
            nodes: Liste der zu prüfenden Nodes
            xml_path: Pfad zur XML-Datei (relativ)
            xsl_id: Tuple der XSL-ID
            exclude_xsl_file: XSL-Datei die ausgeschlossen werden soll

        Returns:
            bool: True wenn die Kombination gefunden wird
        """
        for node in nodes:
            if isinstance(node, XslFile) and node != exclude_xsl_file:
                # Prüfe ob diese XSL-Datei die gleiche ID hat UND die XML-Datei verwendet
                if node.id == xsl_id:
                    for xml_file in node.xmls:
                        if xml_file.xml == xml_path:
                            return True
            elif isinstance(node, TreeNode) and node.children:
                # Rekursiv in Knoten suchen
                if self._check_xml_xsl_combination_recursive(node.children, xml_path, xsl_id, exclude_xsl_file):
                    return True

        return False

    def _delete_pdf_files_for_xml_xsl_combination(self, xml_path: Path, xsl_id: tuple) -> int:
        """
        Löscht alle PDF-Dateien (new, ref, diff) für eine XML+XSL-Kombination.

        Args:
            xml_path: Pfad zur XML-Datei (relativ)
            xsl_id: Tuple der XSL-ID

        Returns:
            int: Anzahl der gelöschten PDF-Dateien
        """
        deleted_count = 0

        try:
            # Erzeuge den PDF-Dateinamen basierend auf XML und XSL-ID
            base_name = xml_path.stem
            xsl_id_str = "_".join(str(x) for x in xsl_id)
            pdf_filename = f"{base_name}_xsl_{xsl_id_str}.pdf"

            # Lösche PDFs in allen drei Verzeichnissen
            pdf_dirs = ["new", "ref", "diff"]
            for dir_name in pdf_dirs:
                pdf_path = self.project.project_dir / dir_name / pdf_filename
                if pdf_path.exists():
                    try:
                        pdf_path.unlink()
                        logger.info(f"PDF-Datei gelöscht: {pdf_path}")
                        deleted_count += 1
                    except Exception as e:
                        logger.warning(f"Konnte PDF-Datei nicht löschen: {pdf_path} - {e}")

            if deleted_count > 0:
                logger.info(f"{deleted_count} PDF-Datei(en) für {xml_path.name} (XSL-ID: {xsl_id}) gelöscht")
            else:
                logger.debug(f"Keine PDF-Dateien für {xml_path.name} (XSL-ID: {xsl_id}) gefunden")

        except Exception as e:
            logger.error(f"Fehler beim Löschen der PDF-Dateien: {e}")

        return deleted_count

    # Kontextmenü-Aktionen für Root-Elemente (Unbekannter Typ)
    def _add_root_tree_node(self):
        """Fügt einen neuen TreeNode als Root-Element hinzu."""
        logger.debug("Neuen TreeNode als Root-Element hinzufügen")
        # TODO: Dialog zum Eingeben der TreeNode-Daten öffnen

    def _collect_parent_params(self, item):
        """
        Sammelt die XSLT-Parameter aller Eltern-Nodes von der Wurzel bis zum angegebenen Item.

        Parameter werden von oben nach unten gesammelt, wobei tiefere Ebenen höhere Priorität haben.

        Args:
            item: Das TreeWidgetItem (kann TreeNode oder XslFile sein)

        Returns:
            dict: Dictionary mit allen gesammelten Parametern (tiefere Ebenen überschreiben höhere)
        """
        parent_params = {}

        try:
            # 1. Projektweite Parameter als Basis (niedrigste Priorität)
            if hasattr(self, "project") and self.project and self.project.xslt_params:
                parent_params.update(self.project.xslt_params)

            # 2. Sammle alle Eltern-Items in einer Liste (von unten nach oben)
            parents = []
            current_item = item.parent()

            while current_item:
                parents.append(current_item)
                current_item = current_item.parent()

            # Kehre Liste um, sodass wir von Wurzel zu Kind iterieren
            parents.reverse()

            # Sammle Parameter von Wurzel zu Kind (Kind überschreibt Eltern)
            for parent_item in parents:
                parent_node = parent_item.data(0, Qt.ItemDataRole.UserRole)

                if parent_node and hasattr(parent_node, "xslt_params") and parent_node.xslt_params:
                    # Update überschreibt vorherige Werte (höhere Priorität für tiefere Ebenen)
                    parent_params.update(parent_node.xslt_params)

            logger.debug(f"Gesammelte Eltern-Parameter: {parent_params}")
            return parent_params

        except Exception as e:
            logger.error(f"Fehler beim Sammeln der Eltern-Parameter: {e}")
            return {}

    def _save_project_settings(self):
        """
        Speichert die aktualisierten Projekt-Einstellungen.
        """
        try:
            # Prüfe ob pdf_project und project existieren
            if not self.pdf_project:
                logger.warning("Keine Projekt-Einstellungen zum Speichern verfügbar")
                return

            if not self.project or not self.project.project_dir:
                logger.warning("Kein Projekt-Verzeichnis zum Speichern verfügbar")
                return

            start_time = time.time()

            # Expand-Status nur speichern, wenn kein Suchfilter aktiv ist
            # (sonst werden die durch die Suche erzwungenen Expansionen gespeichert)
            if not self.ui.searchEdit.text().strip():
                self._save_expanded_state()

            # Speichere in project.yaml im Projekt-Verzeichnis
            self.pdf_project.writeSettings(project_dir=self.project.project_dir)

            dump_time = time.time() - start_time
            logger.debug(f"Performance: Projekt-Einstellungen gespeichert in {dump_time:.3f}s")

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

    def _save_expanded_state(self):
        """
        Speichert die IDs aller aufgeklappten Knoten (TreeNode und XslFile) in den Projekteinstellungen.
        """
        if not hasattr(self, "pdf_project") or not self.pdf_project:
            logger.warning("Keine Projekt-Einstellungen zum Speichern des Expand-Status")
            return

        try:
            expanded_node_ids = []

            # Durchlaufe alle Top-Level-Items
            root_count = self.ui.treeWidget.topLevelItemCount()
            for i in range(root_count):
                item = self.ui.treeWidget.topLevelItem(i)
                self._collect_expanded_items(item, expanded_node_ids)

            # Speichere in Projekteinstellungen
            self.pdf_project.expanded_nodes = expanded_node_ids
            logger.info(f"Expand-Status gespeichert: {len(expanded_node_ids)} aufgeklappte Knoten")
            logger.debug(f"Aufgeklappte Knoten-IDs: {expanded_node_ids}")

        except Exception as e:
            logger.error(f"Fehler beim Speichern des Expand-Status: {e}")

    def _collect_expanded_items(self, item: QTreeWidgetItem, expanded_ids: list):
        """
        Sammelt rekursiv die IDs aller aufgeklappten Items.

        Args:
            item: Das zu prüfende TreeWidgetItem
            expanded_ids: Liste zum Sammeln der IDs
        """
        # Hole Node-Objekt
        node = item.data(0, Qt.ItemDataRole.UserRole)
        if not node:
            return

        # Wenn Item aufgeklappt ist, speichere ID
        if item.isExpanded() and hasattr(node, "id"):
            expanded_ids.append(node.id)

        # Rekursiv alle Kinder durchlaufen
        child_count = item.childCount()
        for i in range(child_count):
            child_item = item.child(i)
            self._collect_expanded_items(child_item, expanded_ids)

    def _restore_expanded_state(self):
        """
        Stellt den Expand-Status aller Knoten aus den Projekteinstellungen wieder her.
        """
        if not hasattr(self, "pdf_project") or not self.pdf_project:
            logger.warning("Keine Projekt-Einstellungen zum Wiederherstellen des Expand-Status")
            return

        if not self.pdf_project.expanded_nodes:
            logger.debug("Keine gespeicherten Expand-Status-Informationen vorhanden")
            return

        try:
            expanded_node_ids = set(self.pdf_project.expanded_nodes)
            logger.info(f"Stelle Expand-Status wieder her: {len(expanded_node_ids)} Knoten")
            logger.debug(f"Aufzuklappende Knoten-IDs: {list(expanded_node_ids)}")

            # Durchlaufe alle Top-Level-Items
            root_count = self.ui.treeWidget.topLevelItemCount()
            for i in range(root_count):
                item = self.ui.treeWidget.topLevelItem(i)
                self._expand_items_by_id(item, expanded_node_ids)

        except Exception as e:
            logger.error(f"Fehler beim Wiederherstellen des Expand-Status: {e}")

    def _expand_items_by_id(self, item: QTreeWidgetItem, expanded_ids: set):
        """
        Klappt Items rekursiv auf, wenn ihre ID in expanded_ids enthalten ist.

        Args:
            item: Das zu prüfende TreeWidgetItem
            expanded_ids: Set der IDs, die aufgeklappt werden sollen
        """
        # Hole Node-Objekt
        node = item.data(0, Qt.ItemDataRole.UserRole)
        if not node or not hasattr(node, "id"):
            return

        # Wenn ID in der Liste ist, klappe Item auf
        if node.id in expanded_ids:
            item.setExpanded(True)

        # Rekursiv alle Kinder durchlaufen
        child_count = item.childCount()
        for i in range(child_count):
            child_item = item.child(i)
            self._expand_items_by_id(child_item, expanded_ids)
