"""
Obsolete-Detector — Erkennung veralteter Projekteinträge nach DB-Import.

Reine Analyselogik ohne Qt-Abhängigkeit. Findet XslFile-Einträge im Projekt,
die nicht mehr in der Datenbank vorhanden sind.
"""

from dataclasses import dataclass, field
from pathlib import Path

from conf import TreeNode, XslFile


@dataclass
class ObsoleteXslEntry:
    """Ein XslFile-Eintrag, der nicht mehr in der Datenbank vorhanden ist."""

    xsl_file: XslFile
    parent_node: TreeNode


@dataclass
class ObsoleteGroup:
    """Gruppe veralteter XslFile-Einträge unter einem gemeinsamen Eltern-Pfad."""

    node_path: list[str]
    parent_node: TreeNode
    xsl_entries: list[ObsoleteXslEntry] = field(default_factory=list)


def extract_db_xsl_ids(new_nodes: list[TreeNode]) -> set[tuple]:
    """
    Extrahiert alle XslFile-IDs aus den frisch aus der DB geladenen Nodes.

    Args:
        new_nodes: Nodes aus _process_sql_data()

    Returns:
        Set aller XslFile-IDs (Tupel)
    """
    ids: set[tuple] = set()
    _collect_ids_recursive(new_nodes, ids)
    return ids


def _collect_ids_recursive(nodes: list, ids: set[tuple]) -> None:
    for node in nodes:
        if isinstance(node, XslFile):
            ids.add(node.id)
        elif isinstance(node, TreeNode) and node.children:
            _collect_ids_recursive(node.children, ids)


def find_obsolete_xsl_entries(
    project_nodes: list[TreeNode],
    db_xsl_ids: set[tuple],
) -> list[ObsoleteGroup]:
    """
    Findet alle XslFile-Einträge im Projekt, die nicht mehr in der DB vorhanden sind.

    Args:
        project_nodes: Aktuelle Nodes aus pdf_project.nodes
        db_xsl_ids: Set aller XslFile-IDs aus der DB (von extract_db_xsl_ids)

    Returns:
        Liste von ObsoleteGroup, sortiert nach Hierarchiepfad
    """
    groups: dict[int, ObsoleteGroup] = {}

    _find_obsolete_recursive(project_nodes, db_xsl_ids, path=[], groups=groups)

    # Sortieren nach Pfad für stabile Darstellung
    return sorted(groups.values(), key=lambda g: g.node_path)


def _find_obsolete_recursive(
    nodes: list,
    db_xsl_ids: set[tuple],
    path: list[str],
    groups: dict[int, ObsoleteGroup],
) -> None:
    for node in nodes:
        if isinstance(node, TreeNode):
            _find_obsolete_in_tree_node(node, db_xsl_ids, path, groups)


def _find_obsolete_in_tree_node(
    node: TreeNode,
    db_xsl_ids: set[tuple],
    parent_path: list[str],
    groups: dict[int, ObsoleteGroup],
) -> None:
    current_path = parent_path + [node.bez]

    for child in node.children:
        if isinstance(child, XslFile):
            if child.id not in db_xsl_ids:
                node_id = id(node)
                if node_id not in groups:
                    groups[node_id] = ObsoleteGroup(
                        node_path=current_path,
                        parent_node=node,
                    )
                groups[node_id].xsl_entries.append(ObsoleteXslEntry(xsl_file=child, parent_node=node))
        elif isinstance(child, TreeNode):
            _find_obsolete_in_tree_node(child, db_xsl_ids, current_path, groups)


def remove_empty_tree_nodes(nodes: list) -> list:
    """
    Entfernt rekursiv alle TreeNodes, deren children-Liste nach der Bereinigung leer ist.
    XslFile-Einträge werden immer behalten (sie wurden bereits entfernt oder sind noch gültig).

    Args:
        nodes: Liste von TreeNode|XslFile

    Returns:
        Bereinigte Liste ohne leere TreeNodes
    """
    result = []
    for node in nodes:
        if isinstance(node, TreeNode):
            node.children = remove_empty_tree_nodes(node.children)
            if node.children:
                result.append(node)
            # leere TreeNodes stillschweigend verwerfen
        else:
            result.append(node)
    return result


def collect_unused_xml_files(
    obsolete_groups: list[ObsoleteGroup],
    project_dir: Path,
    is_xml_used_elsewhere_fn,
) -> list[tuple[Path, Path]]:
    """
    Sammelt XML-Dateien der veralteten XslFiles, die nirgends anders mehr verwendet werden.

    WICHTIG: Muss nach dem Entfernen der XslFiles aus dem Modell aufgerufen werden,
    damit is_xml_used_elsewhere_fn korrekte Ergebnisse liefert.

    Args:
        obsolete_groups: Die veralteten Gruppen
        project_dir: Absoluter Pfad zum Projektverzeichnis
        is_xml_used_elsewhere_fn: Callable(xml_path, exclude_xsl_file) -> bool

    Returns:
        Liste von (xml_path_relativ, xml_path_absolut) für nicht mehr verwendete XML-Dateien
    """
    unused: list[tuple[Path, Path]] = []
    seen: set[str] = set()

    for group in obsolete_groups:
        for entry in group.xsl_entries:
            for xml_file_obj in entry.xsl_file.xmls:
                xml_path_str = str(xml_file_obj.xml)
                if xml_path_str in seen:
                    continue
                seen.add(xml_path_str)
                xml_abs = project_dir / xml_file_obj.xml
                if xml_abs.exists():
                    if not is_xml_used_elsewhere_fn(xml_file_obj.xml, entry.xsl_file):
                        unused.append((xml_file_obj.xml, xml_abs))

    return unused
