"""
HashCalculationMixin - Mixin für Hash-Berechnung und XML-Dateiverwaltung.

Dieses Mixin enthält alle Methoden zur blake2b-Hash-Berechnung,
XML-Datei-Zuordnung und Duplikatserkennung für das MainWindow.
"""

import shutil
import time
import logging
from pathlib import Path

from conf import TreeNode, XslFile, XmlFile
from ui.threads import XmlHashCalculatorThread
from utils import calculate_blake2b_hash

logger = logging.getLogger(__name__)


class HashCalculationMixin:
    """
    Mixin für Hash-Berechnung und XML-Dateiverwaltung.

    Dieses Mixin wird von MainWindow verwendet und erwartet folgende Attribute:
    - self.project: Das aktuelle Projekt
    - self.pdf_project: Die Projekt-Daten (ProjectData)
    - self.hash_calculator_thread: Thread für Hash-Berechnung
    - self._save_project_settings(): Methode zum Speichern der Projekt-Einstellungen
    - self._load_nodes_to_tree(): Methode zum Laden der Nodes in den Tree
    """

    def _assign_xml_to_xsl_nodes(self, xml_file_path: Path, selected_xsl_nodes: list):
        """
        Ordnet eine XML-Datei den ausgewählten XSL-Knoten zu.
        Implementiert Hash-basierte Duplikatserkennung und intelligente Dateinamen-Verwaltung.

        Args:
            xml_file_path: Pfad zur XML-Datei
            selected_xsl_nodes: Liste der ausgewählten XSL-Knoten

        Returns:
            dict: Statistiken über die Verarbeitung
        """
        try:
            logger.info(f"Ordne XML-Datei '{xml_file_path.name}' zu {len(selected_xsl_nodes)} XSL-Knoten zu")

            # 1. Hash für die neue XML-Datei berechnen
            file_hash = self._calculate_hash_for_file(xml_file_path)
            if not file_hash:
                logger.warning(f"Hash-Berechnung für {xml_file_path} fehlgeschlagen")

            # 2. Prüfe ob eine XML-Datei mit gleichem Hash bereits im Projekt existiert
            existing_xml = self._find_xml_file_by_hash(file_hash) if file_hash else None

            if existing_xml:
                # 3. Hash-Match gefunden: Ordne vorhandene XML-Datei zu
                logger.info(f"Hash-Duplikat gefunden: {existing_xml.xml} hat gleichen Hash wie {xml_file_path.name}")
                return self._assign_existing_xml_to_nodes(existing_xml, selected_xsl_nodes)
            else:
                # 4. Kein Hash-Match: Verarbeite als neue XML-Datei
                logger.info(f"Keine Hash-Duplikate gefunden für {xml_file_path.name}, verarbeite als neue Datei")
                return self._process_new_xml_file(xml_file_path, selected_xsl_nodes, file_hash)

        except Exception as e:
            error_msg = f"Fehler beim Zuordnen der XML-Datei: {str(e)}"
            logger.error(error_msg)
            return {"status": "error", "error_msg": error_msg}

    def _start_xml_hash_calculation(self):
        """
        Startet die asynchrone Hash-Berechnung für alle XML-Dateien im Projekt.
        """
        try:
            if not hasattr(self, "pdf_project") or not self.pdf_project:
                logger.debug("Keine Projekt-Einstellungen verfügbar für Hash-Berechnung")
                return

            # Sammle alle XML-Dateien aus dem Projekt
            xml_files = self._collect_all_xml_files()

            if not xml_files:
                logger.debug("Keine XML-Dateien für Hash-Berechnung gefunden")
                return

            logger.info(f"Starte Hash-Berechnung für {len(xml_files)} XML-Dateien")

            # Prüfe ob Projekt verfügbar ist
            if not self.project or not self.project.project_dir:
                logger.warning("Kein Projekt-Verzeichnis für Hash-Berechnung verfügbar")
                return

            # Stoppe vorherigen Thread falls noch aktiv
            if self.hash_calculator_thread and self.hash_calculator_thread.isRunning():
                self.hash_calculator_thread.quit()
                self.hash_calculator_thread.wait()

            # Erstelle und starte neuen Hash-Berechnungs-Thread
            self.hash_calculator_thread = XmlHashCalculatorThread(
                project_dir=Path(self.project.project_dir), xml_files=xml_files
            )

            # Verbinde Signale
            self.hash_calculator_thread.hash_calculated.connect(self._on_hash_calculated)
            self.hash_calculator_thread.calculation_finished.connect(self._on_hash_calculation_finished)
            self.hash_calculator_thread.error_occurred.connect(self._on_hash_calculation_error)

            # Starte Thread
            self.hash_calculator_thread.start()

        except Exception as e:
            logger.error(f"Fehler beim Starten der Hash-Berechnung: {e}")

    def _collect_all_xml_files(self) -> list[XmlFile]:
        """
        Sammelt alle XmlFile-Objekte aus der Projektstruktur.

        Returns:
            list[XmlFile]: Liste aller gefundenen XML-Dateien
        """
        xml_files: list[XmlFile] = []
        seen_paths: set = set()

        try:
            if self.pdf_project and self.pdf_project.nodes:
                self._collect_xml_files_recursive(self.pdf_project.nodes, xml_files, seen_paths)

            logger.debug(f"Gesammelt: {len(xml_files)} XML-Dateien")
            return xml_files

        except Exception as e:
            logger.error(f"Fehler beim Sammeln der XML-Dateien: {e}")
            return []

    def _collect_xml_files_recursive(self, nodes, xml_files: list[XmlFile], seen_paths: set):
        """
        Sammelt rekursiv alle XML-Dateien aus den Nodes.

        Args:
            nodes: Liste der zu durchsuchenden Nodes
            xml_files: Liste zum Sammeln der XML-Dateien
            seen_paths: Set bereits gesehener Pfade (verhindert Duplikate)
        """
        for node in nodes:
            if isinstance(node, XslFile) and node.xmls:
                for xml_file in node.xmls:
                    if xml_file.xml not in seen_paths:
                        xml_files.append(xml_file)
                        seen_paths.add(xml_file.xml)
            elif isinstance(node, TreeNode) and node.children:
                self._collect_xml_files_recursive(node.children, xml_files, seen_paths)

    def _on_hash_calculated(self, xml_file: XmlFile, hash_value: str):
        """
        Wird aufgerufen, wenn ein Hash-Wert berechnet wurde.

        Args:
            xml_file: Das XmlFile-Objekt
            hash_value: Der berechnete Hash-Wert mit Präfix
        """
        try:
            # Setze den Hash-Wert
            xml_file.hashsum = hash_value
            logger.debug(f"Hash gesetzt für {xml_file.xml}: {hash_value}")

        except Exception as e:
            logger.error(f"Fehler beim Setzen des Hash-Werts: {e}")

    def _on_hash_calculation_finished(self, processed_count: int, total_count: int):
        """
        Wird aufgerufen, wenn die Hash-Berechnung abgeschlossen ist.

        Args:
            processed_count: Anzahl der verarbeiteten Dateien
            total_count: Gesamtanzahl der Dateien
        """
        try:
            logger.info(f"Hash-Berechnung abgeschlossen: {processed_count}/{total_count} Dateien verarbeitet")

            # Speichere die aktualisierten Projekt-Einstellungen
            if processed_count > 0:
                self._save_project_settings()
                logger.info("Projekt-Einstellungen mit neuen Hash-Werten gespeichert")

        except Exception as e:
            logger.error(f"Fehler beim Abschließen der Hash-Berechnung: {e}")

    def _on_hash_calculation_error(self, xml_file_path: str, error_message: str):
        """
        Wird aufgerufen, wenn ein Fehler bei der Hash-Berechnung auftritt.

        Args:
            xml_file_path: Pfad zur XML-Datei
            error_message: Fehlermeldung
        """
        logger.warning(f"Hash-Berechnungsfehler für {xml_file_path}: {error_message}")

    def _calculate_hash_for_xml_file(self, xml_file: XmlFile):
        """
        Berechnet synchron den Hash für eine einzelne XML-Datei.
        Wird verwendet beim Hinzufügen neuer XML-Dateien.

        Args:
            xml_file: Das XmlFile-Objekt
        """
        try:
            if xml_file.hashsum:
                logger.debug(f"Hash bereits vorhanden für {xml_file.xml}: {xml_file.hashsum}")
                return

            if not self.project or not self.project.project_dir:
                logger.warning("Kein Projekt-Verzeichnis für Hash-Berechnung verfügbar")
                return

            xml_file_path = Path(self.project.project_dir) / xml_file.xml
            hash_value = calculate_blake2b_hash(xml_file_path)
            if hash_value:
                xml_file.hashsum = hash_value
                logger.debug(f"Hash berechnet für {xml_file.xml}: {xml_file.hashsum}")

        except Exception as e:
            logger.error(f"Fehler beim Berechnen des Hash für {xml_file.xml}: {e}")

    def _find_xml_file_by_hash(self, target_hash: str) -> XmlFile | None:
        """
        Sucht eine XML-Datei mit dem angegebenen Hash im gesamten Projekt.

        Args:
            target_hash: Der zu suchende Hash-Wert (mit blake2b: Präfix)

        Returns:
            XmlFile|None: Die gefundene XML-Datei oder None
        """
        try:
            if not target_hash:
                return None

            all_xml_files = self._collect_all_xml_files()

            for xml_file in all_xml_files:
                if xml_file.hashsum == target_hash:
                    logger.debug(f"Hash-Match gefunden: {xml_file.xml} hat Hash {target_hash}")
                    return xml_file

            logger.debug(f"Kein Hash-Match für {target_hash} gefunden")
            return None

        except Exception as e:
            logger.error(f"Fehler bei Hash-Suche für {target_hash}: {e}")
            return None

    def _generate_alternative_filename(self, original_path: Path, xml_dir: Path) -> Path:
        """
        Generiert alternative Dateinamen im Format: datei_1.xml, datei_2.xml, ...

        Args:
            original_path: Ursprünglicher Dateipfad
            xml_dir: Ziel-XML-Verzeichnis

        Returns:
            Path: Pfad mit alternativem Dateinamen
        """
        try:
            base_name = original_path.stem  # "datei"
            extension = original_path.suffix  # ".xml"

            # Sammle einmalig alle verwendeten Dateinamen (Performance-Optimierung)
            all_xml_files = self._collect_all_xml_files()
            used_names = {xml_file.xml.name for xml_file in all_xml_files}

            counter = 1
            while True:
                new_name = f"{base_name}_{counter}{extension}"
                new_path = xml_dir / new_name

                # Prüfe sowohl physische Existenz als auch Verwendung im Projekt (optimierter Set-Lookup)
                if not new_path.exists() and new_name not in used_names:
                    logger.debug(f"Alternativer Dateiname generiert: {new_name}")
                    return new_path

                counter += 1

                # Sicherheitsgrenze um Endlosschleifen zu vermeiden
                if counter > 1000:
                    raise Exception("Zu viele alternative Dateinamen generiert")

        except Exception as e:
            logger.error(f"Fehler beim Generieren alternativer Dateinamen für {original_path}: {e}")
            # Fallback: Zeitstempel verwenden
            timestamp = int(time.time())
            fallback_name = f"{original_path.stem}_{timestamp}{original_path.suffix}"
            return xml_dir / fallback_name

    def _is_filename_used_in_project(self, relative_xml_path: Path) -> bool:
        """
        Prüft ob ein relativer XML-Dateipfad bereits im Projekt verwendet wird.

        Args:
            relative_xml_path: Relativer Pfad zur XML-Datei (z.B. xml/datei_1.xml)

        Returns:
            bool: True wenn der Dateiname bereits verwendet wird
        """
        try:
            all_xml_files = self._collect_all_xml_files()

            for xml_file in all_xml_files:
                if xml_file.xml == relative_xml_path:
                    return True

            return False

        except Exception as e:
            logger.error(f"Fehler beim Prüfen der Dateiname-Verwendung für {relative_xml_path}: {e}")
            return True  # Im Zweifelsfall annehmen, dass der Name verwendet wird

    def _calculate_hash_for_file(self, file_path: Path) -> str | None:
        """Berechnet synchron den blake2b-Hash für eine Datei."""
        return calculate_blake2b_hash(file_path)

    def _assign_existing_xml_to_nodes(self, existing_xml: XmlFile, selected_xsl_nodes: list):
        """
        Ordnet eine bereits vorhandene XML-Datei (basierend auf Hash-Match) den XSL-Knoten zu.

        Args:
            existing_xml: Die bereits vorhandene XML-Datei
            selected_xsl_nodes: Liste der ausgewählten XSL-Knoten

        Returns:
            dict: Statistiken mit 'status', 'added_count', 'existing_file'
        """
        try:
            added_count = 0

            for xsl_node in selected_xsl_nodes:
                # Prüfe ob diese XML-Datei bereits in der XslFile-Node vorhanden ist
                already_assigned = any(xml_file.xml == existing_xml.xml for xml_file in xsl_node.xmls)

                if not already_assigned:
                    # Erstelle neue XmlFile-Referenz mit gleichem Pfad und Hash
                    new_xml_ref = XmlFile(xml=existing_xml.xml, hashsum=existing_xml.hashsum)
                    xsl_node.xmls.append(new_xml_ref)
                    added_count += 1
                    logger.info(f"Vorhandene XML-Datei '{existing_xml.xml}' zu XSL-Node '{xsl_node.bez}' zugeordnet")
                else:
                    logger.debug(f"XML-Datei '{existing_xml.xml}' bereits in XSL-Node '{xsl_node.bez}' vorhanden")

            if added_count > 0:
                # Speichere die aktualisierten Projekt-Einstellungen
                self._save_project_settings()

                # Aktualisiere das TreeWidget
                self._load_nodes_to_tree()

                return {
                    "status": "existing_added",
                    "added_count": added_count,
                    "existing_file": existing_xml.xml.name,
                }
            else:
                return {"status": "already_assigned", "added_count": 0, "existing_file": existing_xml.xml.name}

        except Exception as e:
            error_msg = f"Fehler beim Zuordnen der vorhandenen XML-Datei: {str(e)}"
            logger.error(error_msg)
            return {"status": "error", "error_msg": error_msg}

    def _process_new_xml_file(self, xml_file_path: Path, selected_xsl_nodes: list, file_hash: str | None):
        """
        Verarbeitet eine neue XML-Datei (kein Hash-Match gefunden).

        Args:
            xml_file_path: Pfad zur neuen XML-Datei
            selected_xsl_nodes: Liste der ausgewählten XSL-Knoten
            file_hash: Berechneter Hash der Datei

        Returns:
            dict: Statistiken mit 'status', 'added_count', 'new_file', 'renamed_from'
        """
        try:
            # Prüfe ob Projekt verfügbar ist
            if not self.project or not self.project.project_dir:
                logger.error("Kein Projekt-Verzeichnis für neue XML-Datei verfügbar")
                return {"status": "error", "error_msg": "Kein Projekt-Verzeichnis verfügbar."}

            # 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() or self._is_filename_used_in_project(Path("xml") / xml_file_path.name):
                # Generiere alternative Dateinamen
                alternative_paths = []
                for i in range(1, 6):  # Generiere bis zu 5 Alternativen
                    alt_path = self._generate_alternative_filename(xml_file_path, xml_dir)
                    if alt_path not in alternative_paths:
                        alternative_paths.append(alt_path)

                # Zeige Dialog zur Auswahl des Dateinamens
                selected_path = self._show_filename_selection_dialog(xml_file_path.name, alternative_paths)

                if not selected_path:
                    # Benutzer hat abgebrochen
                    return {"status": "cancelled", "added_count": 0}

                target_xml_path = selected_path

            # 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 Projekt-Verzeichnis)
            relative_xml_path = Path("xml") / target_xml_path.name

            # Füge die XML-Datei zu allen ausgewählten XSL-Knoten hinzu
            added_count = 0
            for xsl_node in selected_xsl_nodes:
                # 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 not existing_xml:
                    # Erstelle neues XmlFile-Objekt mit Hash
                    new_xml_file = XmlFile(xml=relative_xml_path, hashsum=file_hash)
                    xsl_node.xmls.append(new_xml_file)
                    added_count += 1
                    logger.info(f"XML-Datei '{target_xml_path.name}' zu XSL-Node '{xsl_node.bez}' hinzugefügt")
                else:
                    logger.debug(f"XML-Datei '{target_xml_path.name}' bereits in XSL-Node '{xsl_node.bez}' vorhanden")

            if added_count > 0:
                # Speichere die aktualisierten Projekt-Einstellungen
                self._save_project_settings()

                # Aktualisiere das TreeWidget
                self._load_nodes_to_tree()

                return {
                    "status": "new_added",
                    "added_count": added_count,
                    "new_file": target_xml_path.name,
                    "renamed_from": xml_file_path.name if target_xml_path.name != xml_file_path.name else None,
                }
            else:
                return {
                    "status": "already_assigned",
                    "added_count": 0,
                    "new_file": target_xml_path.name,
                }

        except Exception as e:
            error_msg = f"Fehler beim Verarbeiten der neuen XML-Datei: {str(e)}"
            logger.error(error_msg)
            return {"status": "error", "error_msg": error_msg}

    def _show_filename_selection_dialog(self, original_name: str, alternative_paths: list[Path]) -> Path | None:
        """
        Zeigt einen Dialog zur Auswahl eines alternativen Dateinamens.

        Args:
            original_name: Ursprünglicher Dateiname
            alternative_paths: Liste alternativer Pfade

        Returns:
            Path|None: Ausgewählter Pfad oder None bei Abbruch
        """
        try:
            from PySide6.QtWidgets import (
                QDialog,
                QVBoxLayout,
                QLabel,
                QRadioButton,
                QButtonGroup,
                QPushButton,
                QHBoxLayout,
            )

            dialog = QDialog(self)
            dialog.setWindowTitle("Dateiname auswählen")
            dialog.setModal(True)
            dialog.resize(400, 300)

            layout = QVBoxLayout(dialog)

            # Erklärungstext
            info_label = QLabel(
                f"Eine Datei mit dem Namen '{original_name}' existiert bereits.\n\n"
                "Bitte wählen Sie einen alternativen Dateinamen:"
            )
            layout.addWidget(info_label)

            # Radio-Buttons für alternative Namen
            button_group = QButtonGroup(dialog)
            radio_buttons = []

            for i, alt_path in enumerate(alternative_paths):
                radio_button = QRadioButton(alt_path.name)
                if i == 0:  # Ersten als Standard auswählen
                    radio_button.setChecked(True)
                button_group.addButton(radio_button, i)
                radio_buttons.append(radio_button)
                layout.addWidget(radio_button)

            # Buttons
            button_layout = QHBoxLayout()
            ok_button = QPushButton("OK")
            cancel_button = QPushButton("Abbrechen")

            button_layout.addWidget(ok_button)
            button_layout.addWidget(cancel_button)
            layout.addLayout(button_layout)

            # Event-Handler
            ok_button.clicked.connect(dialog.accept)
            cancel_button.clicked.connect(dialog.reject)

            # Dialog anzeigen
            if dialog.exec() == QDialog.DialogCode.Accepted:
                selected_id = button_group.checkedId()
                if 0 <= selected_id < len(alternative_paths):
                    return alternative_paths[selected_id]

            return None

        except Exception as e:
            logger.error(f"Fehler beim Anzeigen des Dateiname-Auswahl-Dialogs: {e}")
            # Fallback: Ersten alternativen Namen verwenden
            return alternative_paths[0] if alternative_paths else None
