#  Copyright 2026 European Union
#  Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
#  SPDX-License-Identifier: EUPL-1.2
"""Tree view for displaying and interacting with processor filters.

:Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
:Description: QTreeView implementation that provides context menus for adding
              and removing filter components.
"""

from __future__ import annotations

from functools import partial
from typing import Iterable, cast

from PySide6.QtCore import QModelIndex, QPoint, Qt, Signal
from PySide6.QtWidgets import QAbstractItemView, QDialog, QMenu, QMessageBox, QTreeView, QWidget

from mafw.db.db_model import mafw_model_register
from mafw.enumerators import LogicalOp
from mafw.steering.models import (
    Condition,
    ConditionalFilterConfig,
    FieldFilterConfig,
    ModelFilterConfig,
    ProcessorConfig,
)
from mafw.steering_gui.dialogs.add_model_dialog import AddModelDialog
from mafw.steering_gui.models.filter_tree_model import FilterNodeType, FilterTreeModel, LogicKey


class FilterTreeView(QTreeView):
    """Tree view for managing filter hierarchy."""

    selection_changed = Signal(QModelIndex)
    filter_changed = Signal()

    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
        self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.customContextMenuRequested.connect(self._show_context_menu)
        self.header().hide()

    def set_model(self, model: FilterTreeModel) -> None:
        """Set the model and expand all nodes."""
        super().setModel(model)
        self.expandAll()
        selection_model = self.selectionModel()
        if selection_model:
            selection_model.currentChanged.connect(self._on_current_changed)

    def _on_current_changed(self, current: QModelIndex, previous: QModelIndex) -> None:
        self.selection_changed.emit(current)

    def _show_context_menu(self, pos: QPoint) -> None:
        index = self.indexAt(pos)
        model = self.model()
        if not isinstance(model, FilterTreeModel):
            return

        menu = QMenu(self)
        node_type = model.get_node_type(index)

        if node_type == FilterNodeType.ROOT:
            add_model_action = menu.addAction('Add Model Filter')
            add_model_action.triggered.connect(self._add_model_filter)
            menu.addAction('Add/Edit Logic').triggered.connect(self._add_root_logic)

        elif node_type == FilterNodeType.MODEL:
            menu.addAction('Add Condition').triggered.connect(partial(self.add_condition, index))
            menu.addAction('Add Field Filter').triggered.connect(partial(self.add_field_filter, index))
            menu.addAction('Add Conditional Filter').triggered.connect(partial(self.add_conditional_filter, index))
            menu.addAction('Add/Edit Logic').triggered.connect(partial(self.add_logic, index))

            menu.addSeparator()
            menu.addAction('Remove').triggered.connect(partial(self.remove_item, index))

        elif node_type == FilterNodeType.FIELD:
            menu.addAction('Add Condition').triggered.connect(partial(self.add_condition, index))
            menu.addAction('Add/Edit Logic').triggered.connect(partial(self.add_logic, index))

            menu.addSeparator()
            menu.addAction('Remove').triggered.connect(partial(self.remove_item, index))

        elif node_type in (FilterNodeType.CONDITION, FilterNodeType.CONDITIONAL, FilterNodeType.LOGIC):
            menu.addAction('Remove').triggered.connect(partial(self.remove_item, index))

        if not menu.isEmpty():
            menu.exec(self.viewport().mapToGlobal(pos))

    def add_root_logic(self) -> None:
        """Add or edit processor-level logic."""
        self._add_root_logic()

    def _add_root_logic(self) -> None:
        model = self.model()
        if isinstance(model, FilterTreeModel):
            if not self._has_logic(model._config):
                model._config.logic_buffer = ''
                model._config.logic_dirty = True
            model.refresh()
            self.expandAll()
            logic_index = model.find_logic_index(LogicKey(model_name=None, field_name=None))
            if logic_index.isValid():
                self.setCurrentIndex(logic_index)
            self.filter_changed.emit()

    def add_model_filter_by_name(self, model_name: str) -> None:
        """Add a ModelFilterConfig for the given model name."""
        model = self.model()
        if not isinstance(model, FilterTreeModel):
            return

        config = model._config
        if model_name not in config.filters:
            config.filters[model_name] = []

        has_model_config = any(isinstance(f, ModelFilterConfig) for f in config.filters[model_name])
        if not has_model_config:
            new_filter = ModelFilterConfig(name=model_name, model=model_name)
            config.filters[model_name].insert(0, new_filter)

        model.refresh()
        self.expandAll()
        model_index = model.find_model_index(model_name)
        if model_index.isValid():
            self.setCurrentIndex(model_index)
        self.filter_changed.emit()

    def _add_model_filter(self) -> None:
        model = self.model()
        if not isinstance(model, FilterTreeModel):
            return

        config = model._config
        advertised = getattr(config, 'schema_filter_models', [])
        all_models = mafw_model_register.get_model_names()

        # setting the dialog parent to None allows the user to move the dialog freely
        dialog = AddModelDialog(advertised, all_models, None)
        if dialog.exec() == QDialog.DialogCode.Accepted:
            name = dialog.selected_model()
            if name:
                self.add_model_filter_by_name(name)

    def add_condition(self, index: QModelIndex, field_name: str | None = None) -> None:
        model = self.model()
        if not isinstance(model, FilterTreeModel):
            return

        data = model.get_item_data(index)
        # parent data should be ModelFilterConfig or FieldFilterConfig

        if isinstance(data, (ModelFilterConfig, FieldFilterConfig)):
            name = field_name
            if name is None:
                if isinstance(data, ModelFilterConfig):
                    name = self._first_unused_model_field(data.model, data.conditions.keys())
                if name is None:
                    existing = set(data.conditions.keys())
                    suffix = 1
                    while f'new_condition_{suffix}' in existing:
                        suffix += 1
                    name = f'new_condition_{suffix}'

            data.conditions[name] = Condition(
                operator=LogicalOp.EQ.value, value=None, is_implicit=False, is_valid=False
            )
            model.refresh()
            self.expandAll()

            warning_key = model.condition_key_for_parent(data, name)
            if warning_key is not None:
                model.set_condition_warning(warning_key, True)
                new_index = model.find_condition_index(warning_key)
                if new_index.isValid():
                    self.setCurrentIndex(new_index)
            self.filter_changed.emit()

    def _first_unused_model_field(self, model_name: str, existing: Iterable[str]) -> str | None:
        """Return the first model field without a configured condition."""

        try:
            model_class = mafw_model_register.get_model(model_name)
        except KeyError:
            return None

        existing_set = set(existing)
        for name in getattr(model_class._meta, 'sorted_field_names', []):  # type: ignore[attr-defined]
            if name not in existing_set:
                return cast(str, name)
        return None

    def add_field_filter(self, index: QModelIndex, field_name: str | None = None) -> None:
        model = self.model()
        if not isinstance(model, FilterTreeModel):
            return

        # index is MODEL type. data is ModelFilterConfig or str.
        # we need the model name.
        item_data = model.get_item_data(index)
        model_name = item_data.model if isinstance(item_data, ModelFilterConfig) else str(item_data)

        processor_config = model._config
        model_config = None
        for f in processor_config.filters.get(model_name, []):
            if isinstance(f, ModelFilterConfig):
                model_config = f
                break

        if field_name is None:
            temp_model_config = model_config
            if temp_model_config is None:
                temp_model_config = ModelFilterConfig(name=model_name, model=model_name)

            from mafw.steering_gui.dialogs.add_field_filter_dialog import AddFieldFilterDialog

            # setting the dialog parent to None allows the user to move the dialog freely
            dialog = AddFieldFilterDialog(temp_model_config, processor_config, None)
            if dialog.exec() == QDialog.DialogCode.Accepted:
                field_name = dialog.selected_field()
                # If we created it, add it now
                if model_config is None:
                    if model_name not in processor_config.filters:
                        processor_config.filters[model_name] = []
                    processor_config.filters[model_name].append(temp_model_config)
            else:
                return

        if field_name:
            if model_name not in processor_config.filters:
                processor_config.filters[model_name] = []

            new_filter = FieldFilterConfig(name=f'{model_name}.{field_name}', model=model_name, field_name=field_name)
            processor_config.filters[model_name].append(new_filter)
            model.refresh()
            self.expandAll()
            self.filter_changed.emit()

            # MOVE SELECTION
            new_index = model.find_index_for_data(new_filter)
            if new_index.isValid():
                self.setCurrentIndex(new_index)

    def add_conditional_filter(self, index: QModelIndex) -> None:
        model = self.model()
        if not isinstance(model, FilterTreeModel):
            return

        item_data = model.get_item_data(index)
        model_name = item_data.model if isinstance(item_data, ModelFilterConfig) else str(item_data)

        config = model._config
        if model_name in config.filters:
            existing_names = {f.name for f in config.filters[model_name] if isinstance(f, ConditionalFilterConfig)}
            base_name = 'conditional'
            i = 1
            name = f'{base_name}_{i}'
            while name in existing_names:
                i += 1
                name = f'{base_name}_{i}'

            new_filter = ConditionalFilterConfig(name=name, model=model_name)
            config.filters[model_name].append(new_filter)
            model.refresh()
            self.expandAll()

            new_index = model.find_index_for_data(new_filter)
            if new_index.isValid():
                self.setCurrentIndex(new_index)

            self.filter_changed.emit()

    def add_logic(self, index: QModelIndex) -> None:
        model = self.model()
        if not isinstance(model, FilterTreeModel):
            return

        data = model.get_item_data(index)
        if isinstance(data, (ModelFilterConfig, FieldFilterConfig)):
            if not self._has_logic(data):
                data.logic_buffer = ''
                data.logic_dirty = True
            model.refresh()
            self.expandAll()
            if isinstance(data, ModelFilterConfig):
                logic_key = LogicKey(model_name=data.model, field_name=None)
            else:
                logic_key = LogicKey(model_name=data.model, field_name=data.field_name)
            logic_index = model.find_logic_index(logic_key)
            if logic_index.isValid():
                self.setCurrentIndex(logic_index)
            self.filter_changed.emit()

    def remove_item(
        self,
        index: QModelIndex,
    ) -> bool:
        """Remove the item at the provided index.

        Returns:
            True if the item was removed, False otherwise.
        """
        model = self.model()
        if not isinstance(model, FilterTreeModel):
            return False

        node_type = model.get_node_type(index)
        data = model.get_item_data(index)

        # Confirm removal
        if QMessageBox.question(self, 'Remove Item', 'Are you sure?') != QMessageBox.StandardButton.Yes:
            return False

        changed = False
        if node_type == FilterNodeType.MODEL:
            # Remove everything for this model?
            # data is ModelFilterConfig or str.
            # If we remove the model node, we remove the entry from config.filters
            model_name = data.model if isinstance(data, ModelFilterConfig) else str(data)
            config = model._config
            if model_name in config.filters:
                del config.filters[model_name]
                changed = True

        elif node_type == FilterNodeType.FIELD:
            # data is FieldFilterConfig
            # need to find parent model name.
            # access parent item in model?
            # Or just use data.model
            model_name = data.model
            config = model._config
            if model_name in config.filters:
                if data in config.filters[model_name]:
                    config.filters[model_name].remove(data)
                    changed = True

        elif node_type == FilterNodeType.CONDITIONAL:
            # data is ConditionalFilterConfig
            model_name = data.model
            config = model._config
            if model_name in config.filters:
                if data in config.filters[model_name]:
                    config.filters[model_name].remove(data)
                    changed = True

        elif node_type == FilterNodeType.CONDITION:
            # Actually I can traverse up.
            parent_index = model.parent(index)
            parent_data = model.get_item_data(parent_index)

            # We also need the key.
            item = index.internalPointer()
            key = item.key

            if isinstance(parent_data, (ModelFilterConfig, FieldFilterConfig)) and key:
                if key in parent_data.conditions:
                    del parent_data.conditions[key]
                    changed = True

        elif node_type == FilterNodeType.LOGIC:
            # data is parent config (ModelFilterConfig or FieldFilterConfig)
            if isinstance(data, (ModelFilterConfig, FieldFilterConfig)):
                data.logic_buffer = None
                data.logic_str_original = None
                data.logic_ast = None
                data.logic_dirty = True
                changed = True
            elif isinstance(data, ProcessorConfig):
                data.logic_buffer = None
                data.logic_str_original = None
                data.logic_ast = None
                data.logic_dirty = True
                changed = True

        if changed:
            model.refresh()
            self.expandAll()
            self.filter_changed.emit()
            return True

        return False

    def _has_logic(self, config: ProcessorConfig | ModelFilterConfig | FieldFilterConfig) -> bool:
        """Return whether the configuration has logic defined or buffered."""

        return config.logic_buffer is not None or config.logic_str_original is not None
