#  Copyright 2026 European Union
#  Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
#  SPDX-License-Identifier: EUPL-1.2
"""Tree model representing the filter configuration hierarchy.

:Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
:Description: QAbstractItemModel implementation for displaying and editing
              filters, conditions, and logic associated with a processor.
"""

from __future__ import annotations

from dataclasses import dataclass
from enum import Enum, auto
from pathlib import Path
from typing import Any, Optional, Union, overload

from PySide6.QtCore import QAbstractItemModel, QModelIndex, QObject, QPersistentModelIndex, Qt
from PySide6.QtGui import QColor, QFont, QIcon

from mafw.steering.models import (
    Condition,
    ConditionalFilterConfig,
    FieldFilterConfig,
    FilterConfig,
    ModelFilterConfig,
    ProcessorConfig,
)

ModelIndex = Union[QModelIndex, QPersistentModelIndex]
_RESOURCE_DIR = Path(__file__).resolve().parent.parent / 'resources'
"""Base path for GUI resources used by the filter tree."""

_WARNING_ICON_PATH = _RESOURCE_DIR / 'mynaui--danger-triangle.svg'
"""Icon path used to flag incomplete conditions."""


@dataclass(frozen=True)
class ConditionKey:
    """Stable key describing a condition entry in the filter tree."""

    model_name: str
    field_name: str | None
    condition_name: str


@dataclass(frozen=True)
class LogicKey:
    """Stable key describing a logic entry in the filter tree."""

    model_name: str | None
    field_name: str | None


class FilterNodeType(Enum):
    """Types of nodes in the filter tree."""

    ROOT = auto()
    MODEL = auto()
    FIELD = auto()
    CONDITION = auto()
    CONDITIONAL = auto()
    LOGIC = auto()


class FilterTreeItem:
    """Internal node for the FilterTreeModel."""

    def __init__(
        self,
        node_type: FilterNodeType,
        data: Any = None,
        parent: Optional[FilterTreeItem] = None,
        key: Optional[str] = None,
    ) -> None:
        self.node_type = node_type
        self.data = data
        self.parent = parent
        self.key = key  # Key for conditions or dict entries
        self.children: list[FilterTreeItem] = []

    def append_child(self, item: FilterTreeItem) -> None:
        self.children.append(item)

    def child(self, row: int) -> Optional[FilterTreeItem]:
        if 0 <= row < len(self.children):
            return self.children[row]
        return None

    def row(self) -> int:
        if self.parent:
            return self.parent.children.index(self)
        return 0


class FilterTreeModel(QAbstractItemModel):
    """Tree model for processor filters."""

    def __init__(self, config: ProcessorConfig, parent: Any = None) -> None:
        super().__init__(parent)
        self._config = config
        self._root_item = FilterTreeItem(FilterNodeType.ROOT)
        self._condition_warnings: set[ConditionKey] = set()
        self._logic_warnings: set[LogicKey] = set()
        self._warning_icon = QIcon(str(_WARNING_ICON_PATH))
        self._setup_model_data()

    def _setup_model_data(self) -> None:
        """Build the tree structure from the ProcessorConfig."""
        self._root_item.children.clear()

        display_name = (
            f'{self._config.name}#{self._config.replica}'
            if hasattr(self._config, 'replica') and self._config.replica
            else self._config.name
        )
        filters_root = FilterTreeItem(FilterNodeType.ROOT, data=f'Filters of {display_name}', parent=self._root_item)
        self._root_item.append_child(filters_root)

        # Add Root Logic if exists
        if self._has_logic(self._config):
            logic_item = FilterTreeItem(FilterNodeType.LOGIC, data=self._config, parent=filters_root)
            filters_root.append_child(logic_item)

        for model_name, filters in self._config.filters.items():
            # Find ModelFilterConfig if exists
            model_config: Optional[ModelFilterConfig] = None
            other_filters: list[FilterConfig] = []

            # We need to find the ModelFilterConfig in the list
            # We assume there is at most one ModelFilterConfig per model name in the list.
            # If there are multiple, we pick the first one? Or merge?
            # Usually one.

            for f in filters:
                if isinstance(f, ModelFilterConfig):
                    model_config = f
                else:
                    other_filters.append(f)

            # Create Model Node
            # If no ModelFilterConfig, we create a virtual one? Or just a node with the name.
            # The node data should ideally be the ModelFilterConfig if it exists.
            # If not, maybe just the model name string?
            # But the user wants to add conditions to it. Adding conditions requires a ModelFilterConfig.
            # So if it doesn't exist, maybe we should create it implicitly or just show it as a group?
            # "Right-clicking on the root ..., allows to add a ModelFilter".
            # This implies if I add a ModelFilter, I create the config.
            # So if it exists, I use it.

            model_node_data = model_config if model_config else model_name
            model_item = FilterTreeItem(FilterNodeType.MODEL, data=model_node_data, parent=filters_root, key=model_name)
            filters_root.append_child(model_item)

            # Add Model Logic if exists
            if model_config and self._has_logic(model_config):
                # Logic node
                logic_item = FilterTreeItem(FilterNodeType.LOGIC, data=model_config, parent=model_item)
                model_item.append_child(logic_item)

            # Add Conditions from ModelFilterConfig
            if model_config:
                for cond_name, cond in model_config.conditions.items():
                    cond_item = FilterTreeItem(FilterNodeType.CONDITION, data=cond, parent=model_item, key=cond_name)
                    model_item.append_child(cond_item)

            # Add other filters (Field, Conditional)
            for f in other_filters:
                if isinstance(f, FieldFilterConfig):
                    field_item = FilterTreeItem(FilterNodeType.FIELD, data=f, parent=model_item)
                    model_item.append_child(field_item)

                    # Logic for field
                    if self._has_logic(f):
                        logic_item = FilterTreeItem(FilterNodeType.LOGIC, data=f, parent=field_item)
                        field_item.append_child(logic_item)

                    # Conditions for field
                    for cond_name, cond in f.conditions.items():
                        cond_item = FilterTreeItem(
                            FilterNodeType.CONDITION, data=f.conditions[cond_name], parent=field_item, key=cond_name
                        )
                        field_item.append_child(cond_item)

                elif isinstance(f, ConditionalFilterConfig):
                    cond_filter_item = FilterTreeItem(FilterNodeType.CONDITIONAL, data=f, parent=model_item)
                    model_item.append_child(cond_filter_item)

    def index(self, row: int, column: int, parent: ModelIndex = QModelIndex()) -> QModelIndex:
        if not self.hasIndex(row, column, parent):
            return QModelIndex()

        if not parent.isValid():
            parent_item = self._root_item
        else:
            parent_item = parent.internalPointer()

        child_item = parent_item.child(row)
        if child_item:
            return self.createIndex(row, column, child_item)
        return QModelIndex()

    @overload
    def parent(self) -> QObject: ...

    @overload
    def parent(self, index: ModelIndex, /) -> QModelIndex: ...

    def parent(self, index: Any = None) -> Any:
        if index is None:
            return super().parent()

        if not index.isValid():
            return QModelIndex()

        child_item = index.internalPointer()
        parent_item = child_item.parent

        if parent_item == self._root_item or parent_item is None:
            return QModelIndex()

        return self.createIndex(parent_item.row(), 0, parent_item)

    def rowCount(self, parent: ModelIndex = QModelIndex()) -> int:  # noqa: N802
        if parent.column() > 0:
            return 0

        if not parent.isValid():
            parent_item = self._root_item
        else:
            parent_item = parent.internalPointer()

        return len(parent_item.children)

    def columnCount(self, parent: ModelIndex = QModelIndex()) -> int:  # noqa: N802
        return 1

    def data(self, index: ModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any:
        if not index.isValid():
            return None

        item: FilterTreeItem = index.internalPointer()
        is_disabled = self._is_item_disabled(item)

        if role == Qt.ItemDataRole.DisplayRole:
            if item.node_type == FilterNodeType.ROOT:
                return str(item.data)
            elif item.node_type == FilterNodeType.MODEL:
                # item.data is either ModelFilterConfig or str
                name = item.data.model if isinstance(item.data, ModelFilterConfig) else str(item.data)
                return f'Model: {name}'
            elif item.node_type == FilterNodeType.FIELD:
                # item.data is FieldFilterConfig
                return f'Field: {item.data.field_name}'
            elif item.node_type == FilterNodeType.CONDITION:
                # item.data is Condition, item.key is the name
                return f'Condition: {item.key}'
            elif item.node_type == FilterNodeType.CONDITIONAL:
                # item.data is ConditionalFilterConfig
                return f'Conditional: {item.data.name}'
            elif item.node_type == FilterNodeType.LOGIC:
                return 'Logic'

        if is_disabled and role == Qt.ItemDataRole.FontRole:
            font = QFont()
            font.setItalic(True)
            return font

        if is_disabled and role == Qt.ItemDataRole.ForegroundRole:
            return QColor(Qt.GlobalColor.gray)

        if role == Qt.ItemDataRole.DecorationRole and item.node_type in (
            FilterNodeType.CONDITION,
            FilterNodeType.CONDITIONAL,
            FilterNodeType.LOGIC,
        ):
            if item.node_type == FilterNodeType.CONDITION:
                condition = item.data
                if isinstance(condition, Condition) and not condition.is_valid and not self._warning_icon.isNull():
                    return self._warning_icon
                condition_key = self.condition_key_for_item(item)
                if condition_key and condition_key in self._condition_warnings and not self._warning_icon.isNull():
                    return self._warning_icon
            elif item.node_type == FilterNodeType.CONDITIONAL:
                data = item.data
                if (
                    isinstance(data, ConditionalFilterConfig)
                    and not data.is_complete()
                    and not self._warning_icon.isNull()
                ):
                    return self._warning_icon
                condition_key = self.condition_key_for_item(item)
                if condition_key and condition_key in self._condition_warnings and not self._warning_icon.isNull():
                    return self._warning_icon
            elif item.node_type == FilterNodeType.LOGIC:
                data = item.data
                if isinstance(data, (ProcessorConfig, ModelFilterConfig, FieldFilterConfig)):
                    if not data.logic_is_valid and not self._warning_icon.isNull():
                        return self._warning_icon
                logic_key = self.logic_key_for_item(item)
                if logic_key and logic_key in self._logic_warnings and not self._warning_icon.isNull():
                    return self._warning_icon

        return None

    def _is_item_disabled(self, item: FilterTreeItem) -> bool:
        current: FilterTreeItem | None = item
        while current is not None:
            data = current.data
            if isinstance(data, FilterConfig) and not data.enabled:
                return True
            current = current.parent
        return False

    def get_node_type(self, index: ModelIndex) -> FilterNodeType | None:
        if not index.isValid():
            return None
        item: FilterTreeItem = index.internalPointer()
        return item.node_type

    def get_item_data(self, index: ModelIndex) -> Any:
        if not index.isValid():
            return None
        item: FilterTreeItem = index.internalPointer()
        return item.data

    def collect_subtree_indexes(self, parent_index: ModelIndex) -> list[ModelIndex]:
        """Return every index below the provided parent."""
        result: list[ModelIndex] = []
        for row in range(self.rowCount(parent_index)):
            child = self.index(row, 0, parent_index)
            if not child.isValid():
                continue
            result.append(child)
            result.extend(self.collect_subtree_indexes(child))
        return result

    def refresh(self) -> None:
        self.beginResetModel()
        self._setup_model_data()
        self._condition_warnings.intersection_update(self._collect_condition_keys())
        self._logic_warnings.intersection_update(self._collect_logic_keys())
        self.endResetModel()

    def find_model_index(self, model_name: str) -> QModelIndex:
        """Find the QModelIndex for the given model name."""
        for root_row in range(self.rowCount()):
            root_index = self.index(root_row, 0)
            root_item = root_index.internalPointer()
            if root_item.node_type != FilterNodeType.ROOT:
                continue
            for model_row in range(self.rowCount(root_index)):
                model_index = self.index(model_row, 0, root_index)
                model_item = model_index.internalPointer()
                if model_item.node_type == FilterNodeType.MODEL and model_item.key == model_name:
                    return model_index
        return QModelIndex()

    def find_index_for_data(self, data: Any, parent_index: QModelIndex = QModelIndex()) -> QModelIndex:
        """Find the QModelIndex for the given data object by searching the tree recursively."""
        for r in range(self.rowCount(parent_index)):
            idx = self.index(r, 0, parent_index)
            item = idx.internalPointer()
            if item.data is data:
                return idx
            # Recurse
            res = self.find_index_for_data(data, idx)
            if res.isValid():
                return res
        return QModelIndex()

    def _collect_condition_keys(self) -> set[ConditionKey]:
        """Return the set of condition keys currently in the tree."""

        keys: set[ConditionKey] = set()
        for root_child in self._root_item.children:
            if root_child.node_type != FilterNodeType.ROOT:
                continue
            for model_child in root_child.children:
                if model_child.node_type != FilterNodeType.MODEL:
                    continue
                for child in model_child.children:
                    if child.node_type == FilterNodeType.CONDITION:
                        key = self.condition_key_for_item(child)
                        if key is not None:
                            keys.add(key)
                    elif child.node_type == FilterNodeType.FIELD:
                        for cond_item in child.children:
                            if cond_item.node_type == FilterNodeType.CONDITION:
                                key = self.condition_key_for_item(cond_item)
                                if key is not None:
                                    keys.add(key)
                    elif child.node_type == FilterNodeType.CONDITIONAL:
                        key = self.condition_key_for_item(child)
                        if key is not None:
                            keys.add(key)
        return keys

    def _collect_logic_keys(self) -> set[LogicKey]:
        """Return the set of logic keys currently in the tree."""

        keys: set[LogicKey] = set()
        for root_child in self._root_item.children:
            if root_child.node_type != FilterNodeType.ROOT:
                continue
            for model_child in root_child.children:
                if model_child.node_type != FilterNodeType.MODEL:
                    continue
                for child in model_child.children:
                    if child.node_type == FilterNodeType.LOGIC:
                        key = self.logic_key_for_item(child)
                        if key is not None:
                            keys.add(key)
                    elif child.node_type == FilterNodeType.FIELD:
                        for field_child in child.children:
                            if field_child.node_type == FilterNodeType.LOGIC:
                                key = self.logic_key_for_item(field_child)
                                if key is not None:
                                    keys.add(key)
        # Root logic
        for root_child in self._root_item.children:
            if root_child.node_type != FilterNodeType.ROOT:
                continue
            for child in root_child.children:
                if child.node_type == FilterNodeType.LOGIC:
                    key = self.logic_key_for_item(child)
                    if key is not None:
                        keys.add(key)
        return keys

    def condition_key_for_parent(
        self,
        parent: ModelFilterConfig | FieldFilterConfig,
        condition_name: str,
    ) -> ConditionKey | None:
        """Return the condition key for the given parent config and name."""

        if isinstance(parent, ModelFilterConfig):
            return ConditionKey(model_name=parent.model, field_name=None, condition_name=condition_name)
        if isinstance(parent, FieldFilterConfig):
            return ConditionKey(model_name=parent.model, field_name=parent.field_name, condition_name=condition_name)
        return None

    def condition_key_for_item(self, item: FilterTreeItem) -> ConditionKey | None:
        """Return a condition key for the provided tree item."""

        if item.node_type == FilterNodeType.CONDITION and item.key:
            parent = item.parent
            if parent is None:
                return None
            parent_data = parent.data
            if isinstance(parent_data, ModelFilterConfig):
                return ConditionKey(model_name=parent_data.model, field_name=None, condition_name=item.key)
            if isinstance(parent_data, FieldFilterConfig):
                return ConditionKey(
                    model_name=parent_data.model,
                    field_name=parent_data.field_name,
                    condition_name=item.key,
                )
        elif item.node_type == FilterNodeType.CONDITIONAL:
            data = item.data
            if isinstance(data, ConditionalFilterConfig):
                # Using a special field_name to avoid collision with ModelFilterConfig conditions
                return ConditionKey(model_name=data.model, field_name='__conditional__', condition_name=data.name)
        return None

    def logic_key_for_item(self, item: FilterTreeItem) -> LogicKey | None:
        """Return a logic key for the provided tree item."""

        if item.node_type != FilterNodeType.LOGIC:
            return None
        parent = item.parent
        if parent is None:
            return None
        if parent.node_type == FilterNodeType.ROOT:
            return LogicKey(model_name=None, field_name=None)
        if parent.node_type == FilterNodeType.MODEL:
            data = parent.data
            model_name = data.model if isinstance(data, ModelFilterConfig) else str(data)
            return LogicKey(model_name=model_name, field_name=None)
        if parent.node_type == FilterNodeType.FIELD:
            data = parent.data
            if isinstance(data, FieldFilterConfig):
                return LogicKey(model_name=data.model, field_name=data.field_name)
        return None

    def condition_key_for_index(self, index: ModelIndex) -> ConditionKey | None:
        """Return a condition key for the provided index."""

        if not index.isValid():
            return None
        item: FilterTreeItem = index.internalPointer()
        return self.condition_key_for_item(item)

    def logic_key_for_index(self, index: ModelIndex) -> LogicKey | None:
        """Return a logic key for the provided index."""

        if not index.isValid():
            return None
        item: FilterTreeItem = index.internalPointer()
        return self.logic_key_for_item(item)

    def set_condition_warning(self, key: ConditionKey, enabled: bool) -> None:
        """Enable or disable the warning icon for the provided condition key."""

        if enabled:
            self._condition_warnings.add(key)
        else:
            self._condition_warnings.discard(key)

        index = self.find_condition_index(key)
        if index.isValid():
            self.dataChanged.emit(index, index, [Qt.ItemDataRole.DecorationRole])

    def set_logic_warning(self, key: LogicKey, enabled: bool) -> None:
        """Enable or disable the warning icon for the provided logic key."""

        if enabled:
            self._logic_warnings.add(key)
        else:
            self._logic_warnings.discard(key)

        index = self.find_logic_index(key)
        if index.isValid():
            self.dataChanged.emit(index, index, [Qt.ItemDataRole.DecorationRole])

    def is_condition_warning(self, key: ConditionKey) -> bool:
        """Return whether the condition key is currently flagged."""

        return key in self._condition_warnings

    def is_logic_warning(self, key: LogicKey) -> bool:
        """Return whether the logic key is currently flagged."""

        return key in self._logic_warnings

    def find_condition_index(self, key: ConditionKey) -> QModelIndex:
        """Return the model index for a condition key if present."""

        root_item = self._root_item
        for root_child in root_item.children:
            if root_child.node_type != FilterNodeType.ROOT:
                continue
            for model_child in root_child.children:
                if model_child.node_type != FilterNodeType.MODEL:
                    continue
                model_data = model_child.data
                model_name = model_data.model if isinstance(model_data, ModelFilterConfig) else str(model_data)
                if model_name != key.model_name:
                    continue
                for child_row, child_item in enumerate(model_child.children):
                    if child_item.node_type == FilterNodeType.FIELD:
                        field_data = child_item.data
                        if not isinstance(field_data, FieldFilterConfig):
                            continue
                        if field_data.field_name != key.field_name:
                            continue
                        for cond_row, cond_item in enumerate(child_item.children):
                            if cond_item.node_type == FilterNodeType.CONDITION and cond_item.key == key.condition_name:
                                return self.createIndex(cond_row, 0, cond_item)
                    elif child_item.node_type == FilterNodeType.CONDITION:
                        if key.field_name is None and child_item.key == key.condition_name:
                            return self.createIndex(child_row, 0, child_item)
                    elif child_item.node_type == FilterNodeType.CONDITIONAL:
                        if key.field_name == '__conditional__' and child_item.data.name == key.condition_name:
                            return self.createIndex(child_row, 0, child_item)

        return QModelIndex()

    def find_logic_index(self, key: LogicKey) -> QModelIndex:
        """Return the model index for a logic key if present."""

        root_item = self._root_item
        for root_child in root_item.children:
            if root_child.node_type != FilterNodeType.ROOT:
                continue
            for model_child in root_child.children:
                if model_child.node_type != FilterNodeType.MODEL:
                    continue
                model_data = model_child.data
                model_name = model_data.model if isinstance(model_data, ModelFilterConfig) else str(model_data)
                for child_row, child_item in enumerate(model_child.children):
                    if child_item.node_type == FilterNodeType.LOGIC and key.field_name is None:
                        if key.model_name == model_name:
                            return self.createIndex(child_row, 0, child_item)
                    if child_item.node_type == FilterNodeType.FIELD:
                        field_data = child_item.data
                        if not isinstance(field_data, FieldFilterConfig):
                            continue
                        if key.model_name != field_data.model or key.field_name != field_data.field_name:
                            continue
                        for logic_row, logic_item in enumerate(child_item.children):
                            if logic_item.node_type == FilterNodeType.LOGIC:
                                return self.createIndex(logic_row, 0, logic_item)
            for child_row, child_item in enumerate(root_child.children):
                if child_item.node_type == FilterNodeType.LOGIC:
                    if key.model_name is None and key.field_name is None:
                        return self.createIndex(child_row, 0, child_item)
        return QModelIndex()

    def _has_logic(self, config: ProcessorConfig | ModelFilterConfig | FieldFilterConfig) -> bool:
        """Return whether the config currently has a logic buffer or original string."""

        if isinstance(config, ProcessorConfig):
            return config.logic_buffer is not None or config.logic_str_original is not None
        return config.logic_buffer is not None or config.logic_str_original is not None
