#  Copyright 2026 European Union
#  Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
#  SPDX-License-Identifier: EUPL-1.2
"""Editors for different filter types in the steering GUI.

:Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
:Description: Collection of widgets for editing model, field, condition, conditional,
              and logic filters.
"""

from __future__ import annotations

import functools
import re
from typing import TYPE_CHECKING, Any, Iterable, cast

from PySide6.QtCore import QAbstractTableModel, QModelIndex, QPersistentModelIndex, QPoint, QStringListModel, Qt, Signal

if TYPE_CHECKING:
    from PySide6.QtCore import SignalInstance


from PySide6.QtGui import QColor, QFont, QSyntaxHighlighter, QTextCharFormat, QTextCursor
from PySide6.QtWidgets import (
    QAbstractItemView,
    QCheckBox,
    QComboBox,
    QCompleter,
    QDialog,
    QDoubleSpinBox,
    QGridLayout,
    QGroupBox,
    QHBoxLayout,
    QHeaderView,
    QLabel,
    QLineEdit,
    QListView,
    QMenu,
    QPlainTextEdit,
    QPushButton,
    QScrollArea,
    QSpinBox,
    QStackedWidget,
    QStyle,
    QTableView,
    QToolButton,
    QVBoxLayout,
    QWidget,
)

from mafw.db.db_filter import ExprParser, ParseError
from mafw.db.db_model import mafw_model_register
from mafw.enumerators import LogicalOp
from mafw.steering.models import (
    Condition,
    ConditionalFilterConfig,
    FieldFilterConfig,
    ModelFilterConfig,
    ModelInfo,
    ProcessorConfig,
    _field_type_from_peewee,
    _normalize_field_type,
)
from mafw.steering_gui.dialogs.add_model_dialog import AddModelDialog
from mafw.steering_gui.utils import FLOAT_DECIMALS, FLOAT_RANGE, INT_RANGE, block_signals, coerce_name

_OPERATOR_TOOLTIPS: dict[LogicalOp, str] = {
    LogicalOp.EQ: 'Equal to',
    LogicalOp.NE: 'Not equal to',
    LogicalOp.LT: 'Less than',
    LogicalOp.LE: 'Less than or equal to',
    LogicalOp.GT: 'Greater than',
    LogicalOp.GE: 'Greater than or equal to',
    LogicalOp.GLOB: 'Glob-style pattern matching',
    LogicalOp.LIKE: 'SQL LIKE pattern matching',
    LogicalOp.REGEXP: 'Regular expression matching',
    LogicalOp.IN: 'Contained in the provided list',
    LogicalOp.NOT_IN: 'Not contained in the provided list',
    LogicalOp.BETWEEN: 'Between the provided bounds',
    LogicalOp.BIT_AND: 'Bitwise AND against the provided value',
    LogicalOp.BIT_OR: 'Bitwise OR against the provided value',
    LogicalOp.IS_NULL: 'Value is NULL',
    LogicalOp.IS_NOT_NULL: 'Value is not NULL',
}
"""Tooltip text for each logical operator."""

_NO_VALUE_OPS = {LogicalOp.IS_NULL, LogicalOp.IS_NOT_NULL}
"""Operators that do not require a value input."""


def _operators_for_type(field_type: type) -> list[LogicalOp]:
    """Return the allowed operators for the given field type."""

    if field_type is str:
        return [
            LogicalOp.EQ,
            LogicalOp.NE,
            LogicalOp.IN,
            LogicalOp.NOT_IN,
            LogicalOp.LIKE,
            LogicalOp.GLOB,
            LogicalOp.REGEXP,
            LogicalOp.IS_NULL,
            LogicalOp.IS_NOT_NULL,
        ]
    if field_type is bool:
        return [LogicalOp.EQ, LogicalOp.NE, LogicalOp.IS_NULL, LogicalOp.IS_NOT_NULL]
    if field_type is float:
        return [
            LogicalOp.EQ,
            LogicalOp.NE,
            LogicalOp.LT,
            LogicalOp.LE,
            LogicalOp.GT,
            LogicalOp.GE,
            LogicalOp.BETWEEN,
            LogicalOp.IN,
            LogicalOp.NOT_IN,
            LogicalOp.IS_NULL,
            LogicalOp.IS_NOT_NULL,
        ]
    return [
        LogicalOp.EQ,
        LogicalOp.NE,
        LogicalOp.LT,
        LogicalOp.LE,
        LogicalOp.GT,
        LogicalOp.GE,
        LogicalOp.BETWEEN,
        LogicalOp.IN,
        LogicalOp.NOT_IN,
        LogicalOp.BIT_AND,
        LogicalOp.BIT_OR,
        LogicalOp.IS_NULL,
        LogicalOp.IS_NOT_NULL,
    ]


def _create_value_widget(field_type: type, parent: QWidget | None = None) -> QWidget:
    """Create an editor widget suitable for the provided field type."""

    if field_type is int:
        spin_box = QSpinBox(parent)
        spin_box.setRange(*INT_RANGE)
        return spin_box
    if field_type is float:
        double_box = QDoubleSpinBox(parent)
        double_box.setRange(*FLOAT_RANGE)
        double_box.setDecimals(FLOAT_DECIMALS)
        return double_box
    if field_type is bool:
        return QCheckBox(parent)
    return QLineEdit(parent)


def _get_widget_value(widget: QWidget) -> Any:
    """Return the current value from a value widget."""

    if isinstance(widget, QSpinBox):
        return widget.value()
    if isinstance(widget, QDoubleSpinBox):
        return widget.value()
    if isinstance(widget, QCheckBox):
        return widget.isChecked()
    if isinstance(widget, QLineEdit):
        return widget.text()
    return None


def _set_widget_value(widget: QWidget, value: Any) -> None:
    """Set the current value on a value widget."""

    if isinstance(widget, QSpinBox):
        try:
            widget.setValue(0 if (value is None or value == '') else int(value))
        except (ValueError, TypeError):
            widget.setValue(0)
        return
    if isinstance(widget, QDoubleSpinBox):
        try:
            widget.setValue(0.0 if (value is None or value == '') else float(value))
        except (ValueError, TypeError):
            widget.setValue(0.0)
        return
    if isinstance(widget, QCheckBox):
        widget.setChecked(bool(value))
        return
    if isinstance(widget, QLineEdit):
        widget.setText('' if value is None else str(value))
        return


class FieldNameWidget(QWidget):
    """Name editor for model-level conditions based on model fields."""

    name_changed = Signal(str)
    type_changed = Signal(object)

    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self._model_info: ModelInfo | None = None
        self._existing_names: set[str] = set()

        layout = QVBoxLayout(self)
        layout.addWidget(QLabel('Field'))
        self._combo = QComboBox()
        self._combo.currentTextChanged.connect(self._on_name_changed)
        self._combo.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContentsOnFirstShow)
        layout.addWidget(self._combo)
        layout.addStretch()

    def set_model_info(self, model_info: ModelInfo | None) -> None:
        """Populate the combo box with fields from the provided model info."""
        self._model_info = model_info
        self._rebuild_items(current=None)

    def set_existing_names(self, names: Iterable[str], current: str | None) -> None:
        """Filter out names already used by sibling conditions."""

        self._existing_names = set(names)
        self._rebuild_items(current=current)

    def _rebuild_items(self, current: str | None) -> None:
        with block_signals(self._combo):
            self._combo.clear()
            if not self._model_info:
                if current:
                    self._combo.addItem(current)
                return

            if current and current not in self._model_info.field_info:
                self._combo.addItem(current)

            for name, info in self._model_info.field_info.items():
                if name in self._existing_names and name != current:
                    continue
                self._combo.addItem(name)
                if info.help_text:
                    index = self._combo.count() - 1
                    self._combo.setItemData(index, info.help_text, Qt.ItemDataRole.ToolTipRole)

            if current and current in self._model_info.field_info:
                index = self._combo.findText(current)
                if index >= 0:
                    self._combo.setCurrentIndex(index)

    def get_name(self) -> str:
        """Return the selected field name."""

        return self._combo.currentText()

    def get_type(self) -> type:
        """Return the python type for the selected field."""
        if self._model_info and self.get_name() in self._model_info.field_info:
            return self._model_info.field_info[self.get_name()].type
        return str

    def set_name(self, name: str) -> None:
        """Set the selected field name."""

        index = self._combo.findText(name)
        if index >= 0:
            self._combo.setCurrentIndex(index)

    def _on_name_changed(self, name: str) -> None:
        self.name_changed.emit(name)
        self.type_changed.emit(self.get_type())


class FreeNameWidget(QWidget):
    """Name editor for field-level conditions with free text."""

    name_changed = Signal(str)
    type_changed = Signal(object)

    def __init__(self, model_name: str, field_name: str, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self._model_name = model_name
        self._field_name = field_name
        self._line_edit = QLineEdit()
        self._line_edit.textChanged.connect(self._on_text_changed)

        layout = QVBoxLayout(self)
        layout.addWidget(QLabel('Condition name'))
        layout.addWidget(self._line_edit)
        layout.addStretch()

    def update_context(self, model_name: str, field_name: str) -> None:
        """Update the model/field context used for typing."""

        self._model_name = model_name
        self._field_name = field_name

    def matches(self, model_name: str, field_name: str) -> bool:
        """Return whether the widget matches the provided context."""

        return self._model_name == model_name and self._field_name == field_name

    def get_name(self) -> str:
        """Return the entered condition name."""

        return self._line_edit.text().strip()

    def set_name(self, name: str) -> None:
        """Set the displayed condition name."""

        self._line_edit.setText(name)

    def get_type(self) -> type:
        """Return the python type for the configured field."""

        try:
            model_class = mafw_model_register.get_model(self._model_name)
        except KeyError:
            return str

        field = model_class._meta.fields.get(self._field_name)  # type: ignore[attr-defined]
        if field is None:
            return str
        return _field_type_from_peewee(field)

    def _on_text_changed(self, text: str) -> None:
        normalized = coerce_name(text)
        if normalized != text:
            with block_signals(self._line_edit):
                self._line_edit.setText(normalized)
        self.name_changed.emit(self.get_name())


class ValueEditorBase(QWidget):
    """Base class for condition value editors."""

    value_changed = Signal()

    def set_field_type(self, field_type: type) -> None:
        """Update the editor to reflect the provided field type."""

        raise NotImplementedError

    def get_value(self) -> Any:
        """Return the current value."""

        raise NotImplementedError

    def set_value(self, value: Any) -> None:
        """Set the current value."""

        raise NotImplementedError


class SingleValueEditor(ValueEditorBase):
    """Editor for a single value condition."""

    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self._field_type: type = str
        self._editor: QWidget = _create_value_widget(self._field_type, self)
        layout = QVBoxLayout(self)
        layout.addWidget(QLabel('Value'))
        layout.addWidget(self._editor)
        layout.addStretch()
        self._wire_value_change(self._editor)

    def set_field_type(self, field_type: type) -> None:
        field_type = _normalize_field_type(field_type)
        if field_type is self._field_type:
            return
        self._field_type = field_type
        layout = self.layout()
        if layout is not None:
            layout.removeWidget(self._editor)
            self._editor.deleteLater()
        self._editor = _create_value_widget(self._field_type, self)
        if layout is not None:
            # We insert at index 1 (after label)
            cast(QVBoxLayout, layout).insertWidget(1, self._editor)
        self._wire_value_change(self._editor)

    def _wire_value_change(self, widget: QWidget) -> None:
        def emit_change(*_: Any) -> None:
            self.value_changed.emit()

        if isinstance(widget, QSpinBox):
            widget.valueChanged.connect(emit_change)
        elif isinstance(widget, QDoubleSpinBox):
            widget.valueChanged.connect(emit_change)
        elif isinstance(widget, QCheckBox):
            widget.toggled.connect(emit_change)
        elif isinstance(widget, QLineEdit):
            widget.textChanged.connect(emit_change)

    def get_value(self) -> Any:
        return _get_widget_value(self._editor)

    def set_value(self, value: Any) -> None:
        _set_widget_value(self._editor, value)


class RangeEditor(ValueEditorBase):
    """Editor for BETWEEN conditions."""

    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self._field_type: type = str
        self._from_widget = _create_value_widget(self._field_type, self)
        self._to_widget = _create_value_widget(self._field_type, self)

        layout = QGridLayout(self)
        layout.addWidget(QLabel('Range'), 0, 0, 1, -1, Qt.AlignmentFlag.AlignCenter)
        layout.addWidget(QLabel('From:'), 1, 0)
        layout.addWidget(self._from_widget, 1, 1, 1, 3)
        layout.addWidget(QLabel('To:'), 2, 0)
        layout.addWidget(self._to_widget, 2, 1, 1, 3)
        layout.setRowStretch(3, 1)

        self._wire_value_change(self._from_widget)
        self._wire_value_change(self._to_widget)

    def set_field_type(self, field_type: type) -> None:
        field_type = _normalize_field_type(field_type)
        if field_type is self._field_type:
            return
        self._field_type = field_type
        layout = self.layout()
        if isinstance(layout, QGridLayout):
            layout.removeWidget(self._from_widget)
            layout.removeWidget(self._to_widget)
            self._from_widget.deleteLater()
            self._to_widget.deleteLater()
            self._from_widget = _create_value_widget(self._field_type, self)
            self._to_widget = _create_value_widget(self._field_type, self)
            # Insert at rows 1 and 2 (after label)
            layout.addWidget(self._from_widget, 1, 1, 1, 3)
            layout.addWidget(self._to_widget, 2, 1, 1, 3)
            self._wire_value_change(self._from_widget)
            self._wire_value_change(self._to_widget)

    def _wire_value_change(self, widget: QWidget) -> None:
        def emit_change(*_: Any) -> None:
            self.value_changed.emit()

        if isinstance(widget, QSpinBox):
            widget.valueChanged.connect(emit_change)
        elif isinstance(widget, QDoubleSpinBox):
            widget.valueChanged.connect(emit_change)
        elif isinstance(widget, QCheckBox):
            widget.toggled.connect(emit_change)
        elif isinstance(widget, QLineEdit):
            widget.textChanged.connect(emit_change)

    def get_value(self) -> list[Any]:
        return [_get_widget_value(self._from_widget), _get_widget_value(self._to_widget)]

    def set_value(self, value: Any) -> None:
        if isinstance(value, (list, tuple)) and len(value) >= 2:
            _set_widget_value(self._from_widget, value[0])
            _set_widget_value(self._to_widget, value[1])
        else:
            _set_widget_value(self._from_widget, None)
            _set_widget_value(self._to_widget, None)


class ListValueRow(QWidget):
    """Row widget for list-based values."""

    value_changed = Signal()
    remove_requested = Signal(object)

    def __init__(self, field_type: type, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self._field_type = _normalize_field_type(field_type)
        self._editor = _create_value_widget(self._field_type, self)
        self._remove_button = QToolButton()
        self._remove_button.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_TitleBarCloseButton))
        self._remove_button.clicked.connect(functools.partial(self.remove_requested.emit, self))

        layout = QHBoxLayout(self)
        layout.addWidget(self._editor)
        layout.addWidget(self._remove_button)
        layout.setContentsMargins(0, 0, 0, 0)

        def emit_change(*_: Any) -> None:
            self.value_changed.emit()

        if isinstance(self._editor, QSpinBox):
            self._editor.valueChanged.connect(emit_change)
        elif isinstance(self._editor, QDoubleSpinBox):
            self._editor.valueChanged.connect(emit_change)
        elif isinstance(self._editor, QCheckBox):
            self._editor.toggled.connect(emit_change)
        elif isinstance(self._editor, QLineEdit):
            self._editor.textChanged.connect(emit_change)

    def set_value(self, value: Any) -> None:
        _set_widget_value(self._editor, value)

    def get_value(self) -> Any:
        return _get_widget_value(self._editor)


class ListEditor(ValueEditorBase):
    """Editor for list-based conditions."""

    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self._field_type: type = str
        self._rows: list[ListValueRow] = []

        main_layout = QVBoxLayout(self)
        main_layout.addWidget(QLabel('Values'))

        self._scroll_area = QScrollArea()
        self._scroll_area.setWidgetResizable(True)
        self._scroll_content = QWidget()
        self._rows_layout = QVBoxLayout(self._scroll_content)
        self._rows_layout.addStretch()
        self._scroll_area.setWidget(self._scroll_content)
        main_layout.addWidget(self._scroll_area)

        self._add_button = QPushButton('+ Add value')
        self._add_button.clicked.connect(self._add_row)
        main_layout.addWidget(self._add_button)
        main_layout.addStretch()

    def set_field_type(self, field_type: type) -> None:
        field_type = _normalize_field_type(field_type)
        if field_type is self._field_type:
            return
        self._field_type = field_type
        for row in list(self._rows):
            self._remove_row(row)
        self._rows.clear()
        self.value_changed.emit()

    def _add_row(self, value: Any = None) -> None:
        row = ListValueRow(self._field_type, self._scroll_content)
        row.set_value(value)
        row.value_changed.connect(self.value_changed.emit)
        row.remove_requested.connect(self._remove_row)
        self._rows.append(row)
        # Insert before the stretch
        self._rows_layout.insertWidget(self._rows_layout.count() - 1, row)
        self.value_changed.emit()

    def _remove_row(self, row: ListValueRow) -> None:
        if row in self._rows:
            self._rows.remove(row)
            self._rows_layout.removeWidget(row)
            row.setParent(None)
            row.deleteLater()
            self.value_changed.emit()

    def get_value(self) -> list[Any]:
        return [row.get_value() for row in self._rows]

    def set_value(self, value: Any) -> None:
        for row in list(self._rows):
            self._remove_row(row)
        if isinstance(value, (list, tuple)):
            for item in value:
                self._add_row(item)


class ProcessorModelsTableModel(QAbstractTableModel):
    """Table model for displaying available processor models."""

    def __init__(
        self,
        processor_config: ProcessorConfig,
        advertised_models: list[str],
        all_models: list[str],
        parent: QWidget | None = None,
    ) -> None:
        super().__init__(parent)
        self._processor_config = processor_config
        self._advertised_models = list(advertised_models)
        advertised_set = set(advertised_models)
        others = sorted([name for name in all_models if name not in advertised_set])
        self._model_names = self._advertised_models + others
        self._headers = ['Name', 'Recommended']

    def rowCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int:
        return len(self._model_names)

    def columnCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int:
        return len(self._headers)

    def headerData(self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole) -> Any:
        if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
            return self._headers[section]
        return None

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

        row = index.row()
        col = index.column()
        model_name = self._model_names[row]

        if role == Qt.ItemDataRole.DisplayRole:
            if col == 0:
                return model_name

        if role == Qt.ItemDataRole.CheckStateRole and col == 1:
            return Qt.CheckState.Checked if model_name in self._advertised_models else Qt.CheckState.Unchecked

        if role == Qt.ItemDataRole.FontRole and col == 0:
            if self._has_model_filter(model_name):
                font = QFont()
                font.setBold(True)
                return font

        return None

    def get_model_name(self, row: int) -> str:
        """Return the model name for the given row."""
        return self._model_names[row]

    def _has_model_filter(self, model_name: str) -> bool:
        filters = self._processor_config.filters.get(model_name, [])
        return any(isinstance(f, ModelFilterConfig) for f in filters)


class ProcessorFilterEditor(QWidget):
    """Editor for processor-level filter configuration."""

    add_model_filter_requested = Signal(str)
    remove_model_filter_requested = Signal(str)
    edit_model_filter_requested = Signal(str)
    add_logic_requested = Signal()

    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self._config: ProcessorConfig | None = None
        self._advertised_models: list[str] = []
        self._all_models: list[str] = []

        layout = QVBoxLayout(self)

        self._title_label = QLabel('Filter for processor: ')
        self._title_label.setStyleSheet('font-weight: bold; font-size: 14px;')
        layout.addWidget(self._title_label)

        layout.addSpacing(10)
        info_title = QLabel('Information:')
        info_title.setStyleSheet('font-weight: bold;')
        layout.addWidget(info_title)

        info_grid = QGridLayout()
        self._info_models = QLabel('Number of model filters: 0')
        self._info_logic = QLabel('Processor logic statement: False')

        info_grid.addWidget(self._info_models, 0, 0)
        info_grid.addWidget(self._info_logic, 0, 1)
        layout.addLayout(info_grid)

        layout.addSpacing(10)
        models_label = QLabel('Available models')
        models_label.setStyleSheet('font-weight: bold;')
        layout.addWidget(models_label)

        self._models_table = QTableView()
        self._models_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
        self._models_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
        self._models_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
        self._models_table.verticalHeader().hide()
        self._models_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
        self._models_table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self._models_table.customContextMenuRequested.connect(self._show_table_context_menu)
        self._models_table.doubleClicked.connect(self._on_table_double_clicked)
        layout.addWidget(self._models_table)

        button_layout = QHBoxLayout()
        self._add_model_btn = QPushButton('Add model filter')
        self._add_model_btn.clicked.connect(self._on_add_model_clicked)
        self._logic_btn = QPushButton('Logic')
        self._logic_btn.clicked.connect(self.add_logic_requested.emit)

        button_layout.addWidget(self._add_model_btn)
        button_layout.addWidget(self._logic_btn)
        layout.addLayout(button_layout)

    def set_config(self, config: ProcessorConfig) -> None:
        """Update the editor with processor configuration data."""
        self._config = config
        self._title_label.setText(f'Filter for processor: {config.name}')
        self._info_models.setText(f'Number of model filters: {self._count_model_filters(config)}')
        self._info_logic.setText(f'Processor logic statement: {self._has_logic(config)}')

        self._advertised_models = list(getattr(config, 'schema_filter_models', []))
        self._all_models = mafw_model_register.get_model_names()
        table_model = ProcessorModelsTableModel(config, self._advertised_models, self._all_models, self)
        self._models_table.setModel(table_model)

    def _show_table_context_menu(self, pos: QPoint) -> None:
        index = self._models_table.indexAt(pos)
        if not index.isValid():
            return

        table_model = cast(ProcessorModelsTableModel, self._models_table.model())
        model_name = table_model.get_model_name(index.row())

        menu = QMenu(self)
        if self._has_model_filter(model_name):
            menu.addAction('Remove model filter').triggered.connect(
                functools.partial(self.remove_model_filter_requested.emit, model_name)
            )
        else:
            menu.addAction('Add model filter').triggered.connect(
                functools.partial(self.add_model_filter_requested.emit, model_name)
            )

        menu.exec(self._models_table.viewport().mapToGlobal(pos))

    def _on_table_double_clicked(self, index: QModelIndex) -> None:
        table_model = cast(ProcessorModelsTableModel, self._models_table.model())
        model_name = table_model.get_model_name(index.row())

        if self._has_model_filter(model_name):
            self.edit_model_filter_requested.emit(model_name)
        else:
            self.add_model_filter_requested.emit(model_name)

    def _on_add_model_clicked(self) -> None:
        if self._config is None:
            return

        # setting the dialog parent to None allows the user to move the dialog freely
        dialog = AddModelDialog(self._advertised_models, self._all_models, None)
        if dialog.exec() == QDialog.DialogCode.Accepted:
            name = dialog.selected_model()
            if not name:
                return
            if self._has_model_filter(name):
                self.edit_model_filter_requested.emit(name)
            else:
                self.add_model_filter_requested.emit(name)

    def _has_model_filter(self, model_name: str) -> bool:
        if self._config is None:
            return False
        filters = self._config.filters.get(model_name, [])
        return any(isinstance(f, ModelFilterConfig) for f in filters)

    @staticmethod
    def _count_model_filters(config: ProcessorConfig) -> int:
        return sum(1 for filters in config.filters.values() for f in filters if isinstance(f, ModelFilterConfig))

    @staticmethod
    def _has_logic(config: ProcessorConfig) -> bool:
        return config.logic_buffer is not None or config.logic_str_original is not None


class ModelFieldsTableModel(QAbstractTableModel):
    """Table model for displaying model fields and their filters."""

    def __init__(
        self,
        model_info: ModelInfo,
        model_config: ModelFilterConfig | None,
        processor_config: ProcessorConfig,
        parent: QWidget | None = None,
    ) -> None:
        super().__init__(parent)
        self._model_info = model_info
        self._model_config = model_config
        self._processor_config = processor_config
        self._field_names = list(model_info.field_info.keys())
        self._headers = ['Field name', 'Field type', 'Existing filter']

    def rowCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int:
        return len(self._field_names)

    def columnCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int:
        return len(self._headers)

    def headerData(self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole) -> Any:
        if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
            return self._headers[section]
        return None

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

        row = index.row()
        col = index.column()
        field_name = self._field_names[row]
        field_info = self._model_info.field_info[field_name]

        filter_type = self._get_filter_type(field_name)

        if role == Qt.ItemDataRole.DisplayRole:
            if col == 0:
                return field_name
            if col == 1:
                return field_info.type.__name__
            if col == 2:
                return filter_type if filter_type else 'None'

        if role == Qt.ItemDataRole.CheckStateRole and col == 2:
            return Qt.CheckState.Checked if filter_type else Qt.CheckState.Unchecked

        if role == Qt.ItemDataRole.ToolTipRole:
            return field_info.help_text

        if role == Qt.ItemDataRole.FontRole:
            if filter_type:
                # We return a font with bold set
                from PySide6.QtGui import QFont

                font = QFont()
                font.setBold(True)
                return font

        return None

    def _get_filter_type(self, field_name: str) -> str | None:
        # Check for simple condition in ModelFilterConfig
        if self._model_config and field_name in self._model_config.conditions:
            return 'Simple'

        # Check for FieldFilterConfig in ProcessorConfig
        model_name = self._model_config.model if self._model_config else ''
        if model_name:
            filters = self._processor_config.filters.get(model_name, [])
            for f in filters:
                if isinstance(f, FieldFilterConfig) and f.field_name == field_name:
                    return 'Composite'
        return None

    def get_field_name(self, row: int) -> str:
        return self._field_names[row]


class ModelFilterEditor(QWidget):
    """Editor for ModelFilterConfig."""

    enabled_changed = Signal(ModelFilterConfig)

    add_condition_requested = Signal(QModelIndex, object)  # index, field_name (str or None)
    add_field_filter_requested = Signal(QModelIndex, object)  # index, field_name (str or None)
    add_logic_requested = Signal(QModelIndex)
    remove_requested = Signal(QModelIndex)
    edit_condition_requested = Signal(QModelIndex, str)  # index, field_name
    remove_condition_requested = Signal(QModelIndex, str)  # index, field_name
    edit_field_filter_requested = Signal(QModelIndex, str)  # index, field_name
    filter_changed = Signal()

    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self._config: ModelFilterConfig | None = None
        self._processor_config: ProcessorConfig | None = None
        self._index: QModelIndex | None = None

        layout = QVBoxLayout(self)

        self._title_label = QLabel('Filter for model: ')
        self._title_label.setStyleSheet('font-weight: bold; font-size: 14px;')
        layout.addWidget(self._title_label)

        self._enable_check = QCheckBox('Enable')
        self._enable_check.toggled.connect(self._on_enable_toggled)
        layout.addWidget(self._enable_check)

        layout.addSpacing(10)
        info_title = QLabel('Information:')
        info_title.setStyleSheet('font-weight: bold;')
        layout.addWidget(info_title)

        info_grid = QGridLayout()
        self._info_conditions = QLabel('Number of conditions: 0')
        self._info_fields = QLabel('Number of field filters: 0')
        self._info_conditionals = QLabel('Number of conditional filters: 0')
        self._info_logic = QLabel('Custom logic statement: False')

        info_grid.addWidget(self._info_conditions, 0, 0)
        info_grid.addWidget(self._info_fields, 0, 1)
        info_grid.addWidget(self._info_conditionals, 1, 0)
        info_grid.addWidget(self._info_logic, 1, 1)
        layout.addLayout(info_grid)

        layout.addSpacing(10)
        fields_label = QLabel('Model fields')
        fields_label.setStyleSheet('font-weight: bold;')
        layout.addWidget(fields_label)

        self._fields_table = QTableView()
        self._fields_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
        self._fields_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
        self._fields_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
        self._fields_table.verticalHeader().hide()
        self._fields_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
        self._fields_table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self._fields_table.customContextMenuRequested.connect(self._show_table_context_menu)
        self._fields_table.doubleClicked.connect(self._on_table_double_clicked)
        layout.addWidget(self._fields_table)

        button_layout = QHBoxLayout()
        self._add_cond_btn = QPushButton('Add condition')
        self._add_cond_btn.clicked.connect(
            functools.partial(self._emit_signal_with_index, self.add_condition_requested)
        )
        self._add_field_btn = QPushButton('Add Field filter')
        self._add_field_btn.clicked.connect(
            functools.partial(self._emit_signal_with_index, self.add_field_filter_requested)
        )
        self._add_logic_btn = QPushButton('Logic')
        self._add_logic_btn.clicked.connect(functools.partial(self._emit_signal_with_index, self.add_logic_requested))
        self._remove_btn = QPushButton('Remove')
        self._remove_btn.clicked.connect(functools.partial(self._emit_signal_with_index, self.remove_requested))

        button_layout.addWidget(self._add_cond_btn)
        button_layout.addWidget(self._add_field_btn)
        button_layout.addWidget(self._add_logic_btn)
        button_layout.addWidget(self._remove_btn)

        layout.addLayout(button_layout)

    def _emit_signal_with_index(self, signal: SignalInstance, field_name: str | None = None) -> None:
        if signal in (self.add_condition_requested, self.add_field_filter_requested):
            signal.emit(self._index, field_name)
        else:
            signal.emit(self._index)

    def set_config(self, data: ModelFilterConfig | str, processor_config: ProcessorConfig, index: QModelIndex) -> None:
        """Update the editor with configuration data."""
        self._processor_config = processor_config
        self._index = index

        model_name = data.model if isinstance(data, ModelFilterConfig) else str(data)
        self._title_label.setText(f'Filter for model: {model_name}')

        if isinstance(data, ModelFilterConfig):
            self._config = data
            self._enable_check.setEnabled(True)
            self._enable_check.blockSignals(True)
            self._enable_check.setChecked(data.enabled)
            self._enable_check.blockSignals(False)
            self._info_conditions.setText(f'Number of conditions: {len(data.conditions)}')
            self._info_logic.setText(f'Custom logic statement: {self._has_logic(data)}')
            self._add_cond_btn.setEnabled(True)
            self._add_logic_btn.setEnabled(True)

            if data.model_info:
                table_model = ModelFieldsTableModel(data.model_info, data, processor_config, self)
                self._fields_table.setModel(table_model)
            else:
                self._fields_table.setModel(None)
        else:
            self._config = None
            self._enable_check.setChecked(False)
            self._enable_check.setEnabled(False)
            self._info_conditions.setText('Number of conditions: 0')
            self._info_logic.setText('Custom logic statement: False')
            self._add_cond_btn.setEnabled(False)
            self._add_logic_btn.setEnabled(False)
            self._fields_table.setModel(None)

        # Global stats for the model
        filters = processor_config.filters.get(model_name, [])
        num_fields = sum(1 for f in filters if isinstance(f, FieldFilterConfig))
        num_conditionals = sum(1 for f in filters if isinstance(f, ConditionalFilterConfig))

        self._info_fields.setText(f'Number of field filters: {num_fields}')
        self._info_conditionals.setText(f'Number of conditional filters: {num_conditionals}')

    @staticmethod
    def _has_logic(config: ModelFilterConfig) -> bool:
        return config.logic_buffer is not None or config.logic_str_original is not None

    def _on_enable_toggled(self, checked: bool) -> None:
        if self._config:
            self._config.enabled = checked
            self.filter_changed.emit()
            self.enabled_changed.emit(self._config)

    def _show_table_context_menu(self, pos: QPoint) -> None:
        index = self._fields_table.indexAt(pos)
        if not index.isValid():
            return

        table_model = cast(ModelFieldsTableModel, self._fields_table.model())
        field_name = table_model.get_field_name(index.row())
        filter_type = table_model._get_filter_type(field_name)

        menu = QMenu(self)
        if filter_type is None:
            menu.addAction('Add condition').triggered.connect(
                functools.partial(self.add_condition_requested.emit, self._index, field_name)
            )
        elif filter_type == 'Simple':
            menu.addAction('Edit condition').triggered.connect(
                functools.partial(self.edit_condition_requested.emit, self._index, field_name)
            )
            menu.addAction('Remove condition').triggered.connect(
                functools.partial(self.remove_condition_requested.emit, self._index, field_name)
            )

        menu.addAction('Add Field Filter').triggered.connect(
            functools.partial(self.add_field_filter_requested.emit, self._index, field_name)
        )

        menu.exec(self._fields_table.viewport().mapToGlobal(pos))

    def _on_table_double_clicked(self, index: QModelIndex) -> None:
        table_model = cast(ModelFieldsTableModel, self._fields_table.model())
        field_name = table_model.get_field_name(index.row())
        filter_type = table_model._get_filter_type(field_name)

        if filter_type == 'Simple':
            self.edit_condition_requested.emit(self._index, field_name)
        elif filter_type == 'Composite':
            self.edit_field_filter_requested.emit(self._index, field_name)
        elif filter_type is None:
            self.add_condition_requested.emit(self._index, field_name)


class ConditionsTableModel(QAbstractTableModel):
    """Table model for displaying conditions in a FieldFilterConfig."""

    def __init__(self, config: FieldFilterConfig, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self._config = config
        self._condition_names = sorted(config.conditions.keys())
        self._headers = ['Condition Name', 'Operator', 'Value']

    def rowCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int:
        return len(self._condition_names)

    def columnCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int:
        return len(self._headers)

    def headerData(self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole) -> Any:
        if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
            return self._headers[section]
        return None

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

        row = index.row()
        col = index.column()
        name = self._condition_names[row]
        condition = self._config.conditions[name]

        if role == Qt.ItemDataRole.DisplayRole:
            if col == 0:
                return name
            if col == 1:
                return str(condition.operator)
            if col == 2:
                return str(condition.value) if condition.value is not None else ''

        return None

    def get_condition_name(self, row: int) -> str:
        return self._condition_names[row]


class FieldFilterEditor(QWidget):
    """Editor for FieldFilterConfig."""

    enabled_changed = Signal(FieldFilterConfig)

    add_condition_requested = Signal(QModelIndex)
    remove_requested = Signal(QModelIndex)
    add_logic_requested = Signal(QModelIndex)
    edit_condition_requested = Signal(QModelIndex, str)
    remove_condition_requested = Signal(QModelIndex, str)
    filter_changed = Signal()

    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self._config: FieldFilterConfig | None = None
        self._tree_index: QModelIndex = QModelIndex()

        layout = QVBoxLayout(self)

        self._title_label = QLabel('Field filter on')
        self._title_label.setStyleSheet('font-weight: bold; font-size: 14px;')
        layout.addWidget(self._title_label)

        self._enable_check = QCheckBox('Enable')
        self._enable_check.toggled.connect(self._on_enable_toggled)
        layout.addWidget(self._enable_check)

        self._help_label = QLabel()
        self._help_label.setWordWrap(True)
        self._help_label.setStyleSheet('font-style: italic; color: gray;')
        self._help_label.setVisible(False)
        layout.addWidget(self._help_label)

        layout.addSpacing(10)
        info_title = QLabel('Information:')
        info_title.setStyleSheet('font-weight: bold;')
        layout.addWidget(info_title)

        info_grid = QGridLayout()
        self._info_conditions = QLabel('Number of conditions: 0')
        self._info_logic = QLabel('Custom logic statement: False')

        info_grid.addWidget(self._info_conditions, 0, 0)
        info_grid.addWidget(self._info_logic, 0, 1)
        layout.addLayout(info_grid)

        layout.addSpacing(10)
        conditions_label = QLabel('Conditions')
        conditions_label.setStyleSheet('font-weight: bold;')
        layout.addWidget(conditions_label)

        self._conditions_table = QTableView()
        self._conditions_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
        self._conditions_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
        self._conditions_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
        self._conditions_table.verticalHeader().hide()
        self._conditions_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
        self._conditions_table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self._conditions_table.customContextMenuRequested.connect(self._show_table_context_menu)
        self._conditions_table.doubleClicked.connect(self._on_table_double_clicked)
        layout.addWidget(self._conditions_table)

        button_layout = QHBoxLayout()
        self._add_cond_btn = QPushButton('Add condition')
        self._add_cond_btn.clicked.connect(functools.partial(self._emit_signal, self.add_condition_requested))
        self._remove_btn = QPushButton('Remove Filter')
        self._remove_btn.clicked.connect(functools.partial(self._emit_signal, self.remove_requested))
        self._logic_btn = QPushButton('Logic')
        self._logic_btn.clicked.connect(functools.partial(self._emit_signal, self.add_logic_requested))

        button_layout.addWidget(self._add_cond_btn)
        button_layout.addWidget(self._remove_btn)
        button_layout.addWidget(self._logic_btn)

        layout.addLayout(button_layout)

    def _emit_signal(self, signal: SignalInstance) -> None:
        if self._tree_index.isValid():
            signal.emit(self._tree_index)

    def set_config(self, data: FieldFilterConfig | str, processor_config: ProcessorConfig, index: QModelIndex) -> None:
        """Update the editor with configuration data."""
        self._tree_index = index

        if isinstance(data, FieldFilterConfig):
            self._config = data

            # Retrieve field info
            field_type_name = 'Unknown'
            help_text = None
            try:
                # in principle we could use the processor config to get to the model info stored in the parent ModelFilterConfig
                # and retrieve from there the field_type and help_text. But it is a bit more labourios than accessing directly
                # the mafw_model_register. We keep the processor_config argument for possible future development.
                model_class = mafw_model_register.get_model(data.model)
                field_obj = model_class._meta.fields.get(data.field_name)  # type: ignore[attr-defined]
                if field_obj:
                    field_type = _field_type_from_peewee(field_obj)
                    field_type_name = field_type.__name__
                    help_text = getattr(field_obj, 'help_text', None)
            except (KeyError, AttributeError):
                pass

            self._title_label.setText(f'Field filter on {data.model} {data.field_name} ({field_type_name})')

            if help_text:
                self._help_label.setText(help_text)
                self._help_label.setVisible(True)
            else:
                self._help_label.setVisible(False)

            self._info_conditions.setText(f'Number of conditions: {len(data.conditions)}')
            self._info_logic.setText(f'Custom logic statement: {self._has_logic(data)}')

            self._enable_check.setEnabled(True)
            self._enable_check.blockSignals(True)
            self._enable_check.setChecked(data.enabled)
            self._enable_check.blockSignals(False)

            self._logic_btn.setEnabled(True)
            self._add_cond_btn.setEnabled(True)
            self._remove_btn.setEnabled(True)

            table_model = ConditionsTableModel(data, self)
            self._conditions_table.setModel(table_model)
        else:
            self._config = None
            self._title_label.setText('Field filter on (No Selection)')
            self._info_conditions.setText('Number of conditions: 0')
            self._info_logic.setText('Custom logic statement: False')
            self._conditions_table.setModel(None)
            self._enable_check.setChecked(False)
            self._enable_check.setEnabled(False)
            self._logic_btn.setEnabled(False)
            self._add_cond_btn.setEnabled(False)
            self._remove_btn.setEnabled(False)

    @staticmethod
    def _has_logic(config: FieldFilterConfig) -> bool:
        return config.logic_buffer is not None or config.logic_str_original is not None

    def _on_enable_toggled(self, checked: bool) -> None:
        if self._config:
            self._config.enabled = checked
            self.filter_changed.emit()
            self.enabled_changed.emit(self._config)

    def _show_table_context_menu(self, pos: QPoint) -> None:
        index = self._conditions_table.indexAt(pos)
        if not index.isValid():
            return

        table_model = cast(ConditionsTableModel, self._conditions_table.model())
        condition_name = table_model.get_condition_name(index.row())

        menu = QMenu(self)
        menu.addAction('Edit condition').triggered.connect(
            functools.partial(self.edit_condition_requested.emit, self._tree_index, condition_name)
        )
        menu.addAction('Remove condition').triggered.connect(
            functools.partial(self.remove_condition_requested.emit, self._tree_index, condition_name)
        )

        menu.exec(self._conditions_table.viewport().mapToGlobal(pos))

    def _on_table_double_clicked(self, index: QModelIndex) -> None:
        table_model = cast(ConditionsTableModel, self._conditions_table.model())
        condition_name = table_model.get_condition_name(index.row())
        self.edit_condition_requested.emit(self._tree_index, condition_name)


class ConditionDefinition(QWidget):
    """
    Reusable widget containing the core condition definition controls:
    Field/Name selection, Operator selection, and Value input.
    """

    condition_changed = Signal()
    condition_renamed = Signal(str, str)

    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self._condition: Condition | None = None
        self._parent_config: ModelFilterConfig | FieldFilterConfig | ConditionalFilterConfig | None = None
        self._condition_name: str | None = None
        self._field_type: type = str

        main_layout = QHBoxLayout(self)
        main_layout.setContentsMargins(0, 0, 0, 0)

        self._name_stack = QStackedWidget()
        self._field_name_widget = FieldNameWidget()
        self._free_name_widget: FreeNameWidget | None = None
        self._name_stack.addWidget(self._field_name_widget)

        self._operator_stack = QStackedWidget()
        self._operator_container = QWidget()
        op_layout = QVBoxLayout(self._operator_container)
        op_layout.addWidget(QLabel('Operator'))
        self._operator_combo = QComboBox()
        self._operator_combo.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContentsOnFirstShow)
        self._operator_combo.currentIndexChanged.connect(self._on_operator_changed)
        op_layout.addWidget(self._operator_combo)
        op_layout.addStretch()
        self._operator_stack.addWidget(self._operator_container)

        self._value_stack = QStackedWidget()
        self._single_editor = SingleValueEditor()
        self._range_editor = RangeEditor()
        self._list_editor = ListEditor()
        self._value_stack.addWidget(self._single_editor)
        self._value_stack.addWidget(self._range_editor)
        self._value_stack.addWidget(self._list_editor)

        for editor in (self._single_editor, self._range_editor, self._list_editor):
            editor.value_changed.connect(self._on_value_changed)

        main_layout.addWidget(self._name_stack, stretch=1)
        main_layout.addWidget(self._operator_stack, stretch=1)
        main_layout.addWidget(self._value_stack, stretch=2)

        self._field_name_widget.name_changed.connect(self._on_name_changed)
        self._field_name_widget.type_changed.connect(self._on_type_changed)

    def set_condition(
        self,
        condition: Condition,
        parent: ModelFilterConfig | FieldFilterConfig | ConditionalFilterConfig,
        condition_name: str,
    ) -> None:
        """Bind the editor to the provided condition."""

        self._condition = condition
        self._parent_config = parent
        self._condition_name = condition_name

        widgets_to_block: list[QWidget] = [self._field_name_widget, self._operator_combo]
        if self._free_name_widget is not None:
            widgets_to_block.append(self._free_name_widget)
        widgets_to_block.extend([self._single_editor, self._range_editor, self._list_editor])

        with block_signals(*widgets_to_block):
            if isinstance(parent, (ModelFilterConfig, ConditionalFilterConfig)):
                self._field_name_widget.set_model_info(parent.model_info)
                # For ConditionalFilterConfig, condition_name is the field name.
                # Since 'conditions' dict doesn't exist on ConditionalFilterConfig in the same way,
                # we skip `set_existing_names` filtering or adapt it.
                # However, set_existing_names logic is mainly to avoid duplicate keys in ModelFilterConfig.conditions.
                # In ConditionalFilterConfig, fields can be reused (e.g. IF a>10 THEN a<20).
                # So we can pass an empty existing names list or just the current name.

                existing_names = parent.conditions.keys() if isinstance(parent, ModelFilterConfig) else []
                self._field_name_widget.set_existing_names(existing_names, condition_name)
                self._field_name_widget.set_name(condition_name)
                self._name_stack.setCurrentWidget(self._field_name_widget)
                self._field_type = self._field_name_widget.get_type()
            else:
                if self._free_name_widget is None:
                    self._free_name_widget = FreeNameWidget(parent.model, parent.field_name)
                    self._free_name_widget.name_changed.connect(self._on_name_changed)
                    self._free_name_widget.type_changed.connect(self._on_type_changed)
                    self._name_stack.addWidget(self._free_name_widget)
                elif not self._free_name_widget.matches(parent.model, parent.field_name):
                    self._free_name_widget.update_context(parent.model, parent.field_name)
                self._free_name_widget.set_name(condition_name)
                self._name_stack.setCurrentWidget(self._free_name_widget)
                self._field_type = self._free_name_widget.get_type()

            self._populate_operator_combo(self._field_type, condition.operator)
            self._update_value_editor(self._current_operator())
            self._set_value_from_condition(condition.value)

        self._update_condition_validity()

    def is_complete(self) -> bool:
        """Return whether the current condition has a valid value selection."""

        if self._condition is None:
            return False
        return self._condition.is_valid

    def _populate_operator_combo(self, field_type: type, current_op: str | LogicalOp) -> None:
        self._operator_combo.clear()
        for op in _operators_for_type(field_type):
            self._operator_combo.addItem(op.value, op)
            tooltip = _OPERATOR_TOOLTIPS.get(op)
            if tooltip:
                index = self._operator_combo.count() - 1
                self._operator_combo.setItemData(index, tooltip, Qt.ItemDataRole.ToolTipRole)

        normalized = self._normalize_operator(current_op)
        if normalized is None:
            normalized = _operators_for_type(field_type)[0]
        index = self._operator_combo.findData(normalized)
        if index >= 0:
            self._operator_combo.setCurrentIndex(index)

    def _normalize_operator(self, op: str | LogicalOp) -> LogicalOp | None:
        if isinstance(op, LogicalOp):
            return op
        if isinstance(op, str) and op.startswith('LogicalOp.'):
            op = op.split('LogicalOp.', 1)[-1]
        try:
            return LogicalOp(op)
        except Exception:
            return None

    def _current_operator(self) -> LogicalOp:
        op = self._operator_combo.currentData()
        if isinstance(op, LogicalOp):
            return op
        return _operators_for_type(self._field_type)[0]

    def _on_operator_changed(self, _: int) -> None:
        if self._condition is None:
            return
        op = self._current_operator()
        self._condition.operator = op.value
        self._condition.is_implicit = False
        self._update_value_editor(op)
        if op in _NO_VALUE_OPS:
            self._condition.value = None
        else:
            self._condition.value = self._current_value()
        self._update_condition_validity()
        self.condition_changed.emit()

    def _on_name_changed(self, new_name: str) -> None:
        if not self._condition_name or not self._condition or not self._parent_config:
            return
        if not new_name:
            return
        if new_name == self._condition_name:
            return
        self.condition_renamed.emit(self._condition_name, new_name)
        self._condition_name = new_name

    def _on_type_changed(self, field_type: type) -> None:
        self._field_type = _normalize_field_type(field_type)
        current_op = self._current_operator()
        self._populate_operator_combo(self._field_type, current_op)
        self._update_value_editor(self._current_operator())
        self._on_value_changed()

    def _update_value_editor(self, op: LogicalOp) -> None:
        editor: ValueEditorBase
        if op == LogicalOp.BETWEEN:
            editor = self._range_editor
        elif op in (LogicalOp.IN, LogicalOp.NOT_IN):
            editor = self._list_editor
        else:
            editor = self._single_editor
        editor.set_field_type(self._field_type)
        self._value_stack.setCurrentWidget(editor)
        self._value_stack.setEnabled(op not in _NO_VALUE_OPS)

    def _set_value_from_condition(self, value: Any) -> None:
        current_editor = self._value_stack.currentWidget()
        if isinstance(current_editor, ValueEditorBase):
            with block_signals(current_editor):
                current_editor.set_value(value)
            if self._condition is not None:
                self._condition.value = self._current_value()

    def _current_value(self) -> Any:
        current_editor = self._value_stack.currentWidget()
        if isinstance(current_editor, ValueEditorBase):
            return current_editor.get_value()
        return None

    def _on_value_changed(self) -> None:
        if self._condition is None:
            return
        op = self._current_operator()
        if op in _NO_VALUE_OPS:
            self._condition.value = None
        else:
            self._condition.value = self._current_value()
        self._condition.operator = op.value
        self._condition.is_implicit = False
        self._update_condition_validity()
        self.condition_changed.emit()

    def _update_condition_validity(self) -> None:
        if self._condition is None:
            return
        op = self._current_operator()
        value = self._condition.value
        self._condition.is_valid = self._is_value_complete(op, value)

    def _is_value_complete(self, op: LogicalOp, value: Any) -> bool:
        if op in _NO_VALUE_OPS:
            return True
        if op == LogicalOp.BETWEEN:
            if not isinstance(value, (list, tuple)) or len(value) < 2:
                return False
            return value[0] is not None and value[1] is not None
        if op in (LogicalOp.IN, LogicalOp.NOT_IN):
            if not isinstance(value, (list, tuple)) or len(value) == 0:
                return False
            if self._field_type is str:
                return all(isinstance(item, str) and item.strip() for item in value)
            return all(item is not None for item in value)
        if self._field_type is str:
            return isinstance(value, str) and value.strip() != ''
        return value is not None


class ConditionEditor(QWidget):
    """Editor for Condition."""

    condition_changed = Signal()
    condition_renamed = Signal(str, str)
    delete_requested = Signal()
    add_condition_requested = Signal()
    add_logic_requested = Signal()

    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent)

        main_layout = QVBoxLayout(self)
        self._definition = ConditionDefinition()
        main_layout.addWidget(self._definition)

        button_layout = QHBoxLayout()
        self._delete_btn = QPushButton('Remove condition')
        self._delete_btn.clicked.connect(self.delete_requested.emit)
        self._add_btn = QPushButton('Add new condition')
        self._add_btn.clicked.connect(self.add_condition_requested.emit)
        self._logic_btn = QPushButton('Logic')
        self._logic_btn.clicked.connect(self.add_logic_requested.emit)

        button_layout.addWidget(self._delete_btn)
        button_layout.addWidget(self._add_btn)
        button_layout.addWidget(self._logic_btn)
        button_layout.addStretch()
        main_layout.addLayout(button_layout)

        self._definition.condition_changed.connect(self.condition_changed.emit)
        self._definition.condition_renamed.connect(self.condition_renamed.emit)

    def set_condition(
        self,
        condition: Condition,
        parent: ModelFilterConfig | FieldFilterConfig | ConditionalFilterConfig,
        condition_name: str,
    ) -> None:
        """Bind the editor to the provided condition."""
        self._definition.set_condition(condition, parent, condition_name)

    def is_complete(self) -> bool:
        """Return whether the current condition has a valid value selection."""
        return self._definition.is_complete()


class ConditionalFilterEditor(QWidget):
    """Editor for ConditionalFilterConfig."""

    filter_changed = Signal()
    remove_requested = Signal(QModelIndex)
    enabled_changed = Signal(ConditionalFilterConfig)

    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self._config: ConditionalFilterConfig | None = None
        self._index: QModelIndex = QModelIndex()

        # Main Layout
        main_layout = QVBoxLayout(self)

        # Scroll Area
        self._scroll_area = QScrollArea()
        self._scroll_area.setWidgetResizable(True)
        self._scroll_content = QWidget()
        self._scroll_layout = QVBoxLayout(self._scroll_content)
        self._scroll_area.setWidget(self._scroll_content)
        main_layout.addWidget(self._scroll_area)

        # Top Part: Label and Filter Name
        name_layout = QHBoxLayout()
        self._model_label = QLabel('Model Name')
        self._model_label.setStyleSheet('font-weight: bold;')
        name_layout.addWidget(self._model_label)

        self._name_edit = QLineEdit()
        self._name_edit.setPlaceholderText('conditional_1')
        self._name_edit.textChanged.connect(self._on_name_changed)
        name_layout.addWidget(self._name_edit)

        self._scroll_layout.addLayout(name_layout)

        self._enable_check = QCheckBox('Enable')
        self._enable_check.toggled.connect(self._on_enable_toggled)
        self._scroll_layout.addWidget(self._enable_check)

        # Groups
        self._if_group = QGroupBox('If-condition')
        self._then_group = QGroupBox('Then-condition')
        self._else_group = QGroupBox('Else-condition')
        self._else_group.setCheckable(True)
        self._else_group.setChecked(False)
        self._else_group.toggled.connect(self._on_else_toggled)

        self._scroll_layout.addWidget(self._if_group)
        self._scroll_layout.addWidget(self._then_group)
        self._scroll_layout.addWidget(self._else_group)

        # Condition Definitions
        self._if_definition = ConditionDefinition()
        self._then_definition = ConditionDefinition()
        self._else_definition = ConditionDefinition()

        # Connect signals
        for definition in (self._if_definition, self._then_definition, self._else_definition):
            definition.condition_changed.connect(self.filter_changed.emit)

        self._if_definition.condition_renamed.connect(self._on_if_renamed)
        self._then_definition.condition_renamed.connect(self._on_then_renamed)
        self._else_definition.condition_renamed.connect(self._on_else_renamed)

        QVBoxLayout(self._if_group).addWidget(self._if_definition)
        QVBoxLayout(self._then_group).addWidget(self._then_definition)
        QVBoxLayout(self._else_group).addWidget(self._else_definition)

        self._scroll_layout.addStretch()

        # Bottom: Remove Button
        self._remove_btn = QPushButton('Remove')
        self._remove_btn.clicked.connect(self._on_remove_clicked)
        main_layout.addWidget(self._remove_btn)

    def set_config(self, data: ConditionalFilterConfig, processor_config: ProcessorConfig, index: QModelIndex) -> None:
        self._config = data
        self._index = index

        self._model_label.setText(f'Model: {data.model}')
        self._enable_check.blockSignals(True)
        self._enable_check.setChecked(data.enabled)
        self._enable_check.blockSignals(False)
        with block_signals(self._name_edit):
            self._name_edit.setText(data.name)

        # Ensure defaults if conditions are missing
        if not self._config.condition:
            self._config.condition = Condition(operator=LogicalOp.EQ.value, value='')
            self._config.condition_field = self._get_default_field()

        if not self._config.then_clause:
            self._config.then_clause = Condition(operator=LogicalOp.EQ.value, value='')
            self._config.then_field = self._get_default_field()

        # Setup Else clause
        with block_signals(self._else_group):
            if self._config.else_clause:
                self._else_group.setChecked(True)
                self._else_definition.set_condition(
                    self._config.else_clause, self._config, self._config.else_field or ''
                )
            else:
                self._else_group.setChecked(False)
                dummy_cond = Condition(operator=LogicalOp.EQ.value, value='')
                dummy_field = self._get_default_field()
                self._else_definition.set_condition(dummy_cond, self._config, dummy_field)

        self._if_definition.set_condition(self._config.condition, self._config, self._config.condition_field)
        self._then_definition.set_condition(self._config.then_clause, self._config, self._config.then_field)

    def _get_default_field(self) -> str:
        if self._config and self._config.model_info and self._config.model_info.field_info:
            return next(iter(self._config.model_info.field_info.keys()))
        return ''

    def _on_name_changed(self, text: str) -> None:
        if self._config:
            normalized = coerce_name(text)
            if normalized != text:
                with block_signals(self._name_edit):
                    self._name_edit.setText(normalized)
            self._config.name = normalized
            self.filter_changed.emit()

    def _on_else_toggled(self, checked: bool) -> None:
        if not self._config:
            return

        if checked:
            self._config.else_clause = self._else_definition._condition
            self._config.else_field = self._else_definition._condition_name
        else:
            self._config.else_clause = None
            self._config.else_field = None

        self.filter_changed.emit()

    def _on_enable_toggled(self, checked: bool) -> None:
        if self._config:
            self._config.enabled = checked
            self.filter_changed.emit()
            self.enabled_changed.emit(self._config)

    def _on_if_renamed(self, old: str, new: str) -> None:
        if self._config:
            self._config.condition_field = new
            self.filter_changed.emit()

    def _on_then_renamed(self, old: str, new: str) -> None:
        if self._config:
            self._config.then_field = new
            self.filter_changed.emit()

    def _on_else_renamed(self, old: str, new: str) -> None:
        if self._config and self._else_group.isChecked():
            self._config.else_field = new
            self.filter_changed.emit()

    def _on_remove_clicked(self) -> None:
        self.remove_requested.emit(self._index)


class LogicTextEdit(QPlainTextEdit):
    """Plain text editor with QCompleter support for logic conditions."""

    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self._completer: QCompleter | None = None

    def set_completer(self, completer: QCompleter) -> None:
        self._completer = completer
        completer.setWidget(self)
        completer.activated.connect(self._insert_completion)

    def _insert_completion(self, completion: str) -> None:
        cursor = self.textCursor()
        start, end = self._word_bounds(cursor.position())
        cursor.setPosition(start)
        cursor.setPosition(end, QTextCursor.MoveMode.KeepAnchor)
        cursor.insertText(completion)
        self.setTextCursor(cursor)

    def _word_bounds(self, position: int) -> tuple[int, int]:
        text = self.toPlainText()
        if not text:
            return position, position
        left = position
        while left > 0 and re.match(r'[A-Za-z0-9_\.\:]', text[left - 1]):
            left -= 1
        right = position
        while right < len(text) and re.match(r'[A-Za-z0-9_\.\:]', text[right]):
            right += 1
        return left, right

    def _text_under_cursor(self) -> str:
        cursor = self.textCursor()
        start, end = self._word_bounds(cursor.position())
        if start == end:
            return ''
        return self.toPlainText()[start:end]

    def keyPressEvent(self, event: Any) -> None:  # noqa: N802
        popup = self._completer.popup() if self._completer else None
        if popup and popup.isVisible():
            if event.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return, Qt.Key.Key_Escape, Qt.Key.Key_Tab):
                event.ignore()
                return
        super().keyPressEvent(event)
        if not self._completer:
            return
        completion_prefix = self._text_under_cursor()
        if not completion_prefix:
            popup = self._completer.popup()
            if popup:
                popup.hide()
            return
        self._completer.setCompletionPrefix(completion_prefix)
        popup = self._completer.popup()
        if popup:
            popup.setCurrentIndex(self._completer.completionModel().index(0, 0))
        self._completer.complete()


class LogicSyntaxHighlighter(QSyntaxHighlighter):
    """Syntax highlighter for logic expressions."""

    def __init__(self, parent: Any = None) -> None:
        super().__init__(parent)
        self._operator_format = QTextCharFormat()
        self._operator_format.setFontWeight(QFont.Weight.Bold)

        self._name_format = QTextCharFormat()
        self._name_format.setFontItalic(True)

        self._paren_colors = [
            QColor('#1f77b4'),
            QColor('#ff7f0e'),
            QColor('#2ca02c'),
            QColor('#d62728'),
            QColor('#9467bd'),
        ]

    def highlightBlock(self, text: str) -> None:  # noqa: N802
        operator_pattern = re.compile(r'\b(AND|OR|NOT)\b')
        for match in operator_pattern.finditer(text):
            self.setFormat(match.start(), len(match.group(0)), self._operator_format)

        name_pattern = re.compile(r'\b[A-Za-z_][A-Za-z0-9_\.]*(?:\:[A-Za-z_][A-Za-z0-9_]*)?\b')
        for match in name_pattern.finditer(text):
            if match.group(0) in ('AND', 'OR', 'NOT'):
                continue
            self.setFormat(match.start(), len(match.group(0)), self._name_format)

        level = self.previousBlockState()
        if level < 0:
            level = 0
        for idx, ch in enumerate(text):
            if ch == '(':
                fmt = QTextCharFormat()
                fmt.setForeground(self._paren_colors[level % len(self._paren_colors)])
                self.setFormat(idx, 1, fmt)
                level += 1
            elif ch == ')':
                level = max(level - 1, 0)
                fmt = QTextCharFormat()
                fmt.setForeground(self._paren_colors[level % len(self._paren_colors)])
                self.setFormat(idx, 1, fmt)
        self.setCurrentBlockState(level)


class LogicEditor(QWidget):
    """Editor for Logic expressions."""

    filter_changed = Signal()
    remove_requested = Signal(QModelIndex)
    validation_changed = Signal(QModelIndex, bool)

    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self._config: ProcessorConfig | ModelFilterConfig | FieldFilterConfig | None = None
        self._processor_config: ProcessorConfig | None = None
        self._index: QModelIndex = QModelIndex()
        self._valid_names: list[str] = []
        self._is_valid = False
        self._updating = False

        layout = QVBoxLayout(self)

        self._title_label = QLabel('Logic expression')
        self._title_label.setStyleSheet('font-weight: bold; font-size: 14px;')
        layout.addWidget(self._title_label)

        layout.addSpacing(6)
        expr_label = QLabel('Expression')
        expr_label.setStyleSheet('font-weight: bold;')
        layout.addWidget(expr_label)

        self._text_edit = LogicTextEdit()
        self._text_edit.textChanged.connect(self._on_text_changed)
        layout.addWidget(self._text_edit)

        self._status_label = QLabel('Syntax not validated.')
        self._status_label.setStyleSheet('color: gray;')
        layout.addWidget(self._status_label)

        bottom_layout = QHBoxLayout()
        layout.addLayout(bottom_layout)

        operator_group = QGroupBox('Operators')
        operator_layout = QGridLayout(operator_group)

        self._btn_lparen = QPushButton('(')
        self._btn_rparen = QPushButton(')')
        self._btn_wrap = QPushButton('(...)')
        self._btn_and = QPushButton('AND')
        self._btn_or = QPushButton('OR')
        self._btn_not = QPushButton('NOT')
        self._btn_remove = QPushButton('Remove')

        operator_layout.addWidget(self._btn_lparen, 0, 0)
        operator_layout.addWidget(self._btn_rparen, 0, 1)
        operator_layout.addWidget(self._btn_wrap, 0, 2)
        operator_layout.addWidget(self._btn_and, 1, 0)
        operator_layout.addWidget(self._btn_or, 1, 1)
        operator_layout.addWidget(self._btn_not, 1, 2)
        operator_layout.addWidget(self._btn_remove, 2, 0, 1, 3)

        bottom_layout.addWidget(operator_group)

        conditions_group = QGroupBox('Conditions')
        conditions_layout = QVBoxLayout(conditions_group)
        self._conditions_model = QStringListModel(self)
        self._conditions_view = QListView()
        self._conditions_view.setModel(self._conditions_model)
        self._conditions_view.doubleClicked.connect(self._on_condition_double_clicked)
        conditions_layout.addWidget(self._conditions_view)
        bottom_layout.addWidget(conditions_group)

        self._completer = QCompleter(self._conditions_model, self)
        self._completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
        self._completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
        self._text_edit.set_completer(self._completer)

        LogicSyntaxHighlighter(self._text_edit.document())

        self._btn_lparen.clicked.connect(lambda: self._insert_token('('))
        self._btn_rparen.clicked.connect(lambda: self._insert_token(')'))
        self._btn_wrap.clicked.connect(self._wrap_selection)
        self._btn_and.clicked.connect(lambda: self._insert_token('AND'))
        self._btn_or.clicked.connect(lambda: self._insert_token('OR'))
        self._btn_not.clicked.connect(lambda: self._insert_token('NOT'))
        self._btn_remove.clicked.connect(self._on_remove_clicked)

    def set_config(
        self,
        data: ProcessorConfig | ModelFilterConfig | FieldFilterConfig,
        processor_config: ProcessorConfig,
        index: QModelIndex,
    ) -> None:
        """Bind the editor to the provided configuration data."""

        self._config = data
        self._processor_config = processor_config
        self._index = index

        title_target = self._logic_target_name(data)
        self._title_label.setText(f'Logic expression for {title_target}')

        self._valid_names = self._collect_valid_names(data, processor_config)
        self._conditions_model.setStringList(self._valid_names)

        current_text = self._current_logic_text(data)
        with block_signals(self._text_edit):
            self._text_edit.setPlainText(current_text)
        self._validate(current_text, emit_signal=False)

    def is_valid(self) -> bool:
        """Return whether the current logic expression is valid."""

        return self._is_valid

    def _current_logic_text(self, data: ProcessorConfig | ModelFilterConfig | FieldFilterConfig) -> str:
        if data.logic_buffer is not None:
            return data.logic_buffer
        return data.logic_str_original or ''

    def _logic_target_name(self, data: ProcessorConfig | ModelFilterConfig | FieldFilterConfig) -> str:
        if isinstance(data, ProcessorConfig):
            return data.name
        if isinstance(data, ModelFilterConfig):
            return data.model
        return f'{data.model}.{data.field_name}'

    def _collect_valid_names(
        self,
        data: ProcessorConfig | ModelFilterConfig | FieldFilterConfig,
        processor_config: ProcessorConfig,
    ) -> list[str]:
        if isinstance(data, ProcessorConfig):
            return sorted(processor_config.filters.keys())
        if isinstance(data, ModelFilterConfig):
            names: set[str] = set(data.conditions.keys())
            filters = processor_config.filters.get(data.model, [])
            for f in filters:
                if isinstance(f, FieldFilterConfig):
                    names.add(f.field_name)
                elif isinstance(f, ConditionalFilterConfig):
                    names.add(f.name)
            return sorted(names)
        return sorted(data.conditions.keys())

    def _on_text_changed(self) -> None:
        if self._updating or self._config is None:
            return
        text = self._text_edit.toPlainText()
        normalized = self._normalize_operators(text)
        if normalized != text:
            cursor = self._text_edit.textCursor()
            pos = cursor.position()
            self._updating = True
            with block_signals(self._text_edit):
                self._text_edit.setPlainText(normalized)
            cursor = self._text_edit.textCursor()
            cursor.setPosition(min(pos, len(normalized)))
            self._text_edit.setTextCursor(cursor)
            self._updating = False
        else:
            normalized = text

        self._config.logic_buffer = normalized
        self._config.logic_dirty = True
        self._validate(normalized)
        self.filter_changed.emit()

    def _normalize_operators(self, text: str) -> str:
        def repl(match: re.Match[str]) -> str:
            return match.group(0).upper()

        return re.sub(r'\b(and|or|not)\b', repl, text, flags=re.IGNORECASE)

    def _validate(self, text: str, *, emit_signal: bool = True) -> None:
        is_valid = False
        message = ''

        if not text.strip():
            message = 'Logic expression is empty.'
        else:
            try:
                parser = ExprParser(text, valid_names=self._valid_names)
                ast = parser.parse()
                is_valid = True
                message = 'Syntax valid.'
                if self._config is not None:
                    self._config.logic_ast = ast
            except ParseError as exc:
                message = str(exc)
                if self._config is not None:
                    self._config.logic_ast = None

        self._is_valid = is_valid
        if self._config is not None:
            has_text = bool(text.strip())
            self._config.logic_is_valid = (not has_text) or is_valid
        if is_valid:
            self._status_label.setStyleSheet('color: green;')
        else:
            self._status_label.setStyleSheet('color: red;')
        self._status_label.setText(message)

        if emit_signal:
            self.validation_changed.emit(self._index, is_valid)

    def _insert_token(self, token: str) -> None:
        cursor = self._text_edit.textCursor()
        text = self._text_edit.toPlainText()
        pos = cursor.position()

        if token in ('(', ')'):
            left_space = ''
            if token == '(' and pos > 0 and not text[pos - 1].isspace() and text[pos - 1] != '(':
                left_space = ' '
            cursor.insertText(f'{left_space}{token}')
            return

        left_space = ''
        if pos > 0 and not text[pos - 1].isspace() and text[pos - 1] != '(':
            left_space = ' '
        right_space = ''
        if pos < len(text) and not text[pos].isspace() and text[pos] != ')':
            right_space = ' '
        cursor.insertText(f'{left_space}{token}{right_space}')

    def _wrap_selection(self) -> None:
        cursor = self._text_edit.textCursor()
        if not cursor.hasSelection():
            self._insert_token('(')
            return
        selected = cursor.selectedText()
        cursor.insertText(f'({selected})')

    def _on_condition_double_clicked(self, index: QModelIndex) -> None:
        if not index.isValid():
            return
        name = self._conditions_model.data(index, Qt.ItemDataRole.DisplayRole)
        if isinstance(name, str):
            self._insert_token(name)

    def _on_remove_clicked(self) -> None:
        self.remove_requested.emit(self._index)
