"""
DatabaseMixin - Mixin für Datenbank-Operationen.

Dieses Mixin enthält alle Methoden zur PostgreSQL-Datenbankanbindung
und Datenverarbeitung für das MainWindow.
"""

import sys
import time
import logging
from pathlib import Path

from PySide6.QtCore import QThread, Signal, Qt
from PySide6.QtWidgets import QMessageBox, QProgressDialog

from conf import app_settings, TreeNode, XslFile
from obsolete_detector import (
    collect_unused_xml_files,
    extract_db_xsl_ids,
    find_obsolete_xsl_entries,
    remove_empty_tree_nodes,
)
from ui.ObsoleteEntriesDialog import ObsoleteEntriesDialog

logger = logging.getLogger(__name__)


class DatabaseQueryThread(QThread):
    """Thread für asynchrone Datenbankabfragen."""

    query_completed = Signal(object)  # pl.DataFrame
    query_failed = Signal(str)  # Fehlermeldung

    def __init__(self, sql_query, connection_string):
        super().__init__()
        self.sql_query = sql_query
        self.connection_string = connection_string

    def run(self):
        import polars as pl

        try:
            df = pl.read_database_uri(self.sql_query, self.connection_string, engine="connectorx").sort(
                ["reporttyp_bez", "report_bez", "repfile_bez"]
            )
            self.query_completed.emit(df)
        except Exception as e:
            self.query_failed.emit(str(e))


class DatabaseMixin:
    """
    Mixin für Datenbank-Operationen.

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

    def on_load_from_fn2_clicked(self):
        """
        Wird ausgeführt, wenn der Button "lade aus FN2" geklickt wird.
        Startet SQL-Abfrage asynchron mit Fortschrittsdialog.
        """
        logger.debug("Button 'lade aus FN2' wurde geklickt!")

        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

            # Hole die PostgreSQL-Datenbank-Konfiguration
            db_config = self._get_database_config(self.project.postgre_sql_db_id)
            if not db_config:
                QMessageBox.warning(self, "Warnung", "PostgreSQL-Datenbank-Konfiguration nicht gefunden.")
                return

            # SQL-Abfrage und Connection-String vorbereiten
            sql_query, connection_string = self._prepare_sql_query(db_config)
            if sql_query is None:
                return  # Fehler bereits angezeigt

            # Fortschrittsdialog erstellen
            self._db_progress = QProgressDialog("Verbinde mit Datenbank...", "Abbrechen", 0, 0, self)
            self._db_progress.setWindowTitle("Datenbank-Abfrage")
            self._db_progress.setWindowModality(Qt.WindowModal)
            self._db_progress.setMinimumDuration(0)

            # Query-Thread erstellen und starten
            self._db_query_thread = DatabaseQueryThread(sql_query, connection_string)
            self._db_query_thread.query_completed.connect(self._on_db_query_completed)
            self._db_query_thread.query_failed.connect(self._on_db_query_failed)
            self._db_query_thread.finished.connect(self._cleanup_db_query)
            self._db_progress.canceled.connect(self._on_db_query_canceled)
            self._db_query_thread.start()

        except Exception as e:
            logger.error(f"Fehler beim Laden aus FN2: {e}")
            QMessageBox.critical(self, "Fehler", f"Fehler beim Laden aus FN2:\n{str(e)}")

    def _on_db_query_completed(self, df):
        """Wird aufgerufen, wenn die Datenbankabfrage erfolgreich war."""
        # Ignoriere Ergebnis, falls der Benutzer abgebrochen hat
        if hasattr(self, "_db_progress") and self._db_progress.wasCanceled():
            return

        try:
            new_nodes = self._process_sql_data(df)
            self._merge_nodes_with_existing(new_nodes)
            self._check_and_remove_obsolete_entries(new_nodes)
            self._save_project_settings()
            self._load_nodes_to_tree()
        except Exception as e:
            logger.error(f"Fehler beim Verarbeiten der DB-Daten: {e}")
            QMessageBox.critical(self, "Fehler", f"Fehler beim Verarbeiten der Daten:\n{str(e)}")

    def _on_db_query_failed(self, error_msg):
        """Wird aufgerufen, wenn die Datenbankabfrage fehlgeschlagen ist."""
        if hasattr(self, "_db_progress") and self._db_progress.wasCanceled():
            return

        error = f"Fehler beim Ausführen der SQL-Abfrage: {error_msg}"
        logger.error(error)
        QMessageBox.critical(self, "Fehler", error)

    def _on_db_query_canceled(self):
        """Wird aufgerufen, wenn der Benutzer die Abfrage abbricht."""
        logger.info("Datenbankabfrage vom Benutzer abgebrochen")

    def _cleanup_db_query(self):
        """Räumt nach der Datenbankabfrage auf."""
        if hasattr(self, "_db_progress"):
            self._db_progress.canceled.disconnect(self._on_db_query_canceled)
            self._db_progress.close()
            self._db_progress.deleteLater()
            del self._db_progress
        if hasattr(self, "_db_query_thread"):
            self._db_query_thread.deleteLater()
            del self._db_query_thread

    def _get_database_config(self, db_id):
        """
        Holt die Datenbank-Konfiguration anhand der ID.

        Args:
            db_id: ID der PostgreSQL-Datenbank

        Returns:
            PostgreSqlDb|None: Die Datenbank-Konfiguration oder None
        """
        for db in app_settings.postgresql_dbs:
            if db.id == db_id:
                return db
        return None

    def _prepare_sql_query(self, db_config):
        """
        Bereitet SQL-Abfrage und Connection-String vor.

        Args:
            db_config: PostgreSQL-Datenbank-Konfiguration

        Returns:
            tuple[str, str]|tuple[None, None]: (sql_query, connection_string) oder (None, None) bei Fehler
        """
        try:
            # PyInstaller entpackt Ressourcen nach sys._MEIPASS;
            # im Entwicklungsmodus liegt die Datei relativ zum Repo-Root
            if hasattr(sys, "_MEIPASS"):
                sql_file_path = Path(sys._MEIPASS) / "res" / "data.sql"  # type: ignore[attr-defined]
            else:
                sql_file_path = Path(__file__).parents[3] / "src" / "res" / "data.sql"
            if not sql_file_path.exists():
                QMessageBox.critical(self, "Fehler", f"SQL-Datei nicht gefunden: {sql_file_path}")
                return None, None

            with open(sql_file_path, "r", encoding="utf-8") as f:
                sql_query = f.read()

            logger.debug(f"SQL-Abfrage geladen: {len(sql_query)} Zeichen")

            connection_string = (
                "postgresql://"
                f"{db_config.username}:"
                f"{db_config.password}@"
                f"{db_config.host}:"
                f"{db_config.port}/"
                f"{db_config.database}?"
                f"sslmode={db_config.ssl_mode.value}"
                f"&connect_timeout={db_config.timeout}"
            )

            logger.info(f"Verbinde zu PostgreSQL: {db_config.host}:{db_config.port}/{db_config.database}")

            return sql_query, connection_string
        except Exception as e:
            error_msg = f"Fehler beim Vorbereiten der SQL-Abfrage: {str(e)}"
            logger.error(error_msg)
            QMessageBox.critical(self, "Fehler", error_msg)
            return None, None

    def _process_sql_data(self, df):
        """
        Verarbeitet die SQL-Daten wie in readCsv.py und erstellt Node-Struktur.

        Args:
            df: Polars DataFrame mit den SQL-Ergebnissen

        Returns:
            list[TreeNode]: Liste der erstellten Root-Nodes
        """
        import polars as pl

        try:
            start_time = time.time()

            # Gruppiere die Daten wie in readCsv.py
            ebene_1 = df.group_by(["reporttyp", "reporttyp_bez"]).len()
            ebene_2 = df.group_by(["reporttyp", "report", "report_bez"]).len()
            ebene_3 = df.group_by(["reporttyp", "report", "repfile", "repfile_bez", "xsl_datei"]).len()

            group_time = time.time() - start_time
            logger.debug(f"Performance: Gruppierung in {group_time:.3f}s")

            new_nodes = []

            start_time = time.time()

            # Erstelle Node-Struktur wie in readCsv.py
            for r1 in ebene_1.rows(named=True):
                tn_1 = TreeNode(id=(r1["reporttyp"],), bez=r1["reporttyp_bez"], children=[])
                r1_children = ebene_2.filter(pl.col("reporttyp") == r1["reporttyp"])

                for r2 in r1_children.rows(named=True):
                    tn_2 = TreeNode(id=(r2["reporttyp"], r2["report"]), bez=r2["report_bez"], children=[])
                    r2_children = ebene_3.filter(
                        (pl.col("reporttyp") == r1["reporttyp"]) & (pl.col("report") == r2["report"])
                    )

                    for r3 in r2_children.rows(named=True):
                        x = XslFile(
                            id=(r3["reporttyp"], r3["report"], r3["repfile"]),
                            bez=r3["repfile_bez"],
                            xsl_file=Path(r3["xsl_datei"]),
                            xmls=[],
                        )

                        tn_2.children.append(x)
                    tn_1.children.append(tn_2)
                new_nodes.append(tn_1)

            nodes_time = time.time() - start_time
            logger.debug(f"Performance: Node-Erstellung in {nodes_time:.3f}s")
            logger.info(f"Erstellt: {len(new_nodes)} Root-Nodes")

            return new_nodes

        except Exception as e:
            logger.error(f"Fehler beim Verarbeiten der SQL-Daten: {e}")
            raise

    def _merge_nodes_with_existing(self, new_nodes):
        """
        Merged neue Nodes mit vorhandenen Nodes basierend auf IDs.
        Überschreibt nur einzelne Eigenschaften, nicht ganze Nodes.

        Args:
            new_nodes: Liste der neuen Nodes
        """
        try:
            logger.info("Merge neue Nodes mit vorhandenen...")

            # Erstelle ein Dictionary der neuen Nodes für schnellen Zugriff
            new_nodes_dict = {}
            self._build_nodes_dict(new_nodes, new_nodes_dict)

            logger.debug(f"Neue Nodes Dictionary erstellt: {len(new_nodes_dict)} Einträge")

            # Merge mit vorhandenen Nodes
            if self.pdf_project and self.pdf_project.nodes:
                self._merge_nodes_recursive(self.pdf_project.nodes, new_nodes_dict)

            # Füge komplett neue Root-Nodes hinzu
            if self.pdf_project and self.pdf_project.nodes:
                existing_root_ids = {node.id for node in self.pdf_project.nodes}
                for new_node in new_nodes:
                    if new_node.id not in existing_root_ids:
                        self.pdf_project.nodes.append(new_node)
                        logger.info(f"Neue Root-Node hinzugefügt: {new_node.bez}")
            elif self.pdf_project:
                # Wenn keine Nodes vorhanden sind, füge alle neuen Nodes hinzu
                self.pdf_project.nodes = new_nodes
                logger.info(f"Alle {len(new_nodes)} Root-Nodes hinzugefügt (keine vorhandenen Nodes)")

            logger.info("Merge abgeschlossen")

        except Exception as e:
            logger.error(f"Fehler beim Mergen der Nodes: {e}")
            raise

    def _build_nodes_dict(self, nodes, nodes_dict):
        """
        Erstellt rekursiv ein Dictionary aller Nodes für schnellen ID-basierten Zugriff.

        Args:
            nodes: Liste der Nodes
            nodes_dict: Dictionary zum Füllen
        """
        for node in nodes:
            nodes_dict[node.id] = node

            if isinstance(node, TreeNode) and node.children:
                self._build_nodes_dict(node.children, nodes_dict)

    def _merge_nodes_recursive(self, existing_nodes, new_nodes_dict):
        """
        Merged rekursiv vorhandene Nodes mit neuen Nodes.

        Args:
            existing_nodes: Liste der vorhandenen Nodes
            new_nodes_dict: Dictionary der neuen Nodes
        """
        for existing_node in existing_nodes:
            if existing_node.id in new_nodes_dict:
                new_node = new_nodes_dict[existing_node.id]

                # Aktualisiere nur die Bezeichnung, falls sie sich geändert hat
                if existing_node.bez != new_node.bez:
                    logger.info(
                        f"Aktualisiere Bezeichnung für Node {existing_node.id}: '{existing_node.bez}' -> '{new_node.bez}'"
                    )
                    existing_node.bez = new_node.bez

                # Für XslFile: Aktualisiere xsl_file Pfad
                if isinstance(existing_node, XslFile) and isinstance(new_node, XslFile):
                    if existing_node.xsl_file != new_node.xsl_file:
                        logger.info(
                            f"Aktualisiere XSL-Datei für Node {existing_node.id}: '{existing_node.xsl_file}' -> '{new_node.xsl_file}'"
                        )
                        existing_node.xsl_file = new_node.xsl_file

            # Rekursiv für Knoten (nur bei TreeNode)
            if isinstance(existing_node, TreeNode) and existing_node.children:
                self._merge_nodes_recursive(existing_node.children, new_nodes_dict)

                # Füge neue Knoten hinzu, die noch nicht existieren
                if existing_node.id in new_nodes_dict:
                    new_node = new_nodes_dict[existing_node.id]
                    if isinstance(new_node, TreeNode) and new_node.children:
                        existing_child_ids = {child.id for child in existing_node.children}
                        for new_child in new_node.children:
                            if new_child.id not in existing_child_ids:
                                existing_node.children.append(new_child)
                                logger.info(f"Neues Kind hinzugefügt zu Node {existing_node.id}: {new_child.bez}")

    def _check_and_remove_obsolete_entries(self, new_nodes: list[TreeNode]) -> None:
        """
        Prüft nach dem Merge ob XslFile-Einträge nicht mehr in der DB vorhanden sind.
        Zeigt einen Bestätigungsdialog und entfernt veraltete Einträge inklusive
        PDF- und optionaler XML-Bereinigung.

        Args:
            new_nodes: Frisch aus der DB geladene Nodes (Ergebnis von _process_sql_data)
        """
        if not self.pdf_project or not self.pdf_project.nodes:
            return

        try:
            db_xsl_ids = extract_db_xsl_ids(new_nodes)
            obsolete_groups = find_obsolete_xsl_entries(self.pdf_project.nodes, db_xsl_ids)

            if not obsolete_groups:
                logger.debug("Keine veralteten Einträge gefunden")
                return

            total_count = sum(len(g.xsl_entries) for g in obsolete_groups)
            logger.info(f"{total_count} veraltete XSL-Einträge gefunden")

            dialog = ObsoleteEntriesDialog(self, obsolete_groups)
            if dialog.exec() != ObsoleteEntriesDialog.DialogCode.Accepted:
                logger.info("Entfernung veralteter Einträge vom Benutzer abgebrochen")
                return

            delete_xml = dialog.delete_xml_files()

            # Phase 1: XslFiles aus dem Datenmodell entfernen
            # WICHTIG: Zuerst entfernen, damit _is_xml_xsl_combination_used_elsewhere
            # und _is_xml_file_used_elsewhere die gelöschten Einträge nicht mehr sehen
            # (gleiche Reihenfolge wie in _delete_tree_node)
            for group in obsolete_groups:
                parent = group.parent_node
                obsolete_xsl_ids = {id(entry.xsl_file) for entry in group.xsl_entries}
                parent.children = [c for c in parent.children if id(c) not in obsolete_xsl_ids]
                logger.info(f"{len(group.xsl_entries)} XSL-Einträge aus '{' > '.join(group.node_path)}' entfernt")

            # Phase 2: PDF-Bereinigung für alle entfernten XslFiles
            total_deleted_pdfs = 0
            for group in obsolete_groups:
                for entry in group.xsl_entries:
                    xsl_id = entry.xsl_file.id
                    for xml_file_obj in entry.xsl_file.xmls:
                        is_used = self._is_xml_xsl_combination_used_elsewhere(xml_file_obj.xml, xsl_id, entry.xsl_file)
                        if not is_used:
                            total_deleted_pdfs += self._delete_pdf_files_for_xml_xsl_combination(
                                xml_file_obj.xml, xsl_id
                            )

            if total_deleted_pdfs > 0:
                logger.info(f"{total_deleted_pdfs} PDF-Datei(en) bereinigt")

            # Phase 3: Leere TreeNodes bereinigen (bottom-up durch rekursiven Aufruf)
            self.pdf_project.nodes = remove_empty_tree_nodes(self.pdf_project.nodes)

            # Phase 4: Optionale physische XML-Löschung
            if delete_xml:
                unused_xml = collect_unused_xml_files(
                    obsolete_groups,
                    Path(self.project.project_dir),
                    self._is_xml_file_used_elsewhere,
                )
                for _rel, xml_abs in unused_xml:
                    try:
                        xml_abs.unlink()
                        logger.info(f"Physische XML-Datei gelöscht: {xml_abs}")
                    except Exception as e:
                        logger.warning(f"Konnte XML-Datei nicht löschen: {xml_abs} - {e}")

            logger.info(f"Bereinigung abgeschlossen: {total_count} veraltete Einträge entfernt")

        except Exception as e:
            error_msg = f"Fehler beim Entfernen veralteter Einträge: {str(e)}"
            logger.exception(error_msg)
            QMessageBox.critical(self, "Fehler", error_msg)
