#  Copyright 2026 European Union
#  Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
#  SPDX-License-Identifier: EUPL-1.2
"""Processor parameter editor for steering GUI pipelines.

:Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
:Description: Display and edit processor parameters, replica metadata, and overrides in the GUI.
"""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterable

from PySide6.QtCore import (
    QAbstractItemModel,
    QAbstractTableModel,
    QModelIndex,
    QPersistentModelIndex,
    Qt,
    Signal,
)
from PySide6.QtGui import QColor, QFont, QIcon
from PySide6.QtWidgets import (
    QAbstractItemView,
    QCheckBox,
    QDoubleSpinBox,
    QFileDialog,
    QFormLayout,
    QHBoxLayout,
    QHeaderView,
    QLabel,
    QLineEdit,
    QPushButton,
    QSpinBox,
    QStyledItemDelegate,
    QStyleOptionViewItem,
    QTableView,
    QToolButton,
    QVBoxLayout,
    QWidget,
)

from mafw.steering.models import ParameterConfig, ParameterSchemaStatus, ParameterSource, ProcessorConfig
from mafw.steering_gui.utils import FLOAT_DECIMALS, FLOAT_RANGE, INT_RANGE, block_signals
from mafw.tools.regexp import parse_processor_name

_ACTIVE_COLUMN_LABEL = 'Active'
"""Header label for the parameter active toggle column."""

_COLUMNS = (
    _ACTIVE_COLUMN_LABEL,
    'Name',
    'Value',
    'Type',
    'Source',
    'Status',
)
"""Column labels for the processor parameter table."""

_RESOURCE_DIR = Path(__file__).resolve().parent.parent / 'resources'
_STATUS_ICON_FILES: dict[ParameterSchemaStatus, str] = {
    ParameterSchemaStatus.DEPRECATED: 'mynaui--danger-triangle.svg',
    ParameterSchemaStatus.OK: 'ix--namur-ok.svg',
    ParameterSchemaStatus.UNKNOWN: 'circum--square-question.svg',
    ParameterSchemaStatus.NEW: 'fluent-emoji-high-contrast--new-button.svg',
}


class _ParameterRoles:
    """Custom roles used to expose parameter metadata to delegates."""

    PARAMETER = Qt.ItemDataRole.UserRole + 1
    TYPE = Qt.ItemDataRole.UserRole + 2


@dataclass
class _ParameterRow:
    """Container describing a single parameter table row."""

    config: ParameterConfig
    order: int


class ParameterTableModel(QAbstractTableModel):
    """Qt table model exposing processor parameters for editing."""

    parameter_value_changed = Signal(str, object)
    parameter_active_changed = Signal(str, bool, object)

    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self._rows: list[_ParameterRow] = []
        self._inherited_font = QFont()
        self._inherited_font.setItalic(True)
        self._warning_color = QColor('red')
        self._status_icons = {
            status: QIcon(str(_RESOURCE_DIR / filename)) for status, filename in _STATUS_ICON_FILES.items()
        }
        self._all_row_roles = [
            Qt.ItemDataRole.DisplayRole,
            Qt.ItemDataRole.EditRole,
            Qt.ItemDataRole.CheckStateRole,
            Qt.ItemDataRole.DecorationRole,
            Qt.ItemDataRole.FontRole,
            Qt.ItemDataRole.ForegroundRole,
        ]

    def set_parameters(self, parameters: Iterable[ParameterConfig]) -> None:
        """Replace the rows with the provided parameters."""

        self.beginResetModel()
        self._rows = [_ParameterRow(config=param, order=index) for index, param in enumerate(parameters)]
        self.endResetModel()

    def rowCount(
        self,
        parent: QModelIndex | QPersistentModelIndex = QModelIndex(),
    ) -> int:  # noqa: N802
        if parent.isValid():
            return 0
        return len(self._rows)

    def columnCount(
        self,
        parent: QModelIndex | QPersistentModelIndex = QModelIndex(),
    ) -> int:  # noqa: N802
        if parent.isValid():
            return 0
        return len(_COLUMNS)

    def headerData(
        self,
        section: int,
        orientation: Qt.Orientation,
        role: int = Qt.ItemDataRole.DisplayRole,
    ) -> object:
        if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
            if 0 <= section < len(_COLUMNS):
                return _COLUMNS[section]
        return None

    def data(
        self,
        index: QModelIndex | QPersistentModelIndex,
        role: int = Qt.ItemDataRole.DisplayRole,
    ) -> object:
        if not index.isValid():
            return None
        row = index.row()
        col = index.column()
        if row < 0 or row >= len(self._rows):
            return None
        config = self._rows[row].config

        if role == Qt.ItemDataRole.ToolTipRole:
            return config.help or None

        if role == _ParameterRoles.PARAMETER:
            return config

        if role == _ParameterRoles.TYPE:
            return self._effective_type(config)

        if role == Qt.ItemDataRole.FontRole and config.source == ParameterSource.INHERITED:
            return self._inherited_font

        if (
            role == Qt.ItemDataRole.ForegroundRole
            and isinstance(config.status, ParameterSchemaStatus)
            and config.status in (ParameterSchemaStatus.UNKNOWN, ParameterSchemaStatus.DEPRECATED)
        ):
            return self._warning_color

        if role == Qt.ItemDataRole.CheckStateRole and col == 0:
            return Qt.CheckState.Checked if config.active_override else Qt.CheckState.Unchecked
        if role == Qt.ItemDataRole.CheckStateRole and col == 2 and self._effective_type(config) is bool:
            return Qt.CheckState.Checked if bool(config.value) else Qt.CheckState.Unchecked

        if role == Qt.ItemDataRole.DecorationRole and col == 5:
            icon = self._status_icons.get(config.status)
            if icon and not icon.isNull():
                return icon

        if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole):
            if col == 2 and self._effective_type(config) is bool:
                if role == Qt.ItemDataRole.DisplayRole:
                    return ''
                return bool(config.value)
            return self._data_for_column(config, col, role)

        return None

    def flags(self, index: QModelIndex | QPersistentModelIndex) -> Qt.ItemFlag:
        if not index.isValid():
            return Qt.ItemFlag.ItemIsEnabled
        flags = Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable
        if index.column() == 0:
            return flags | Qt.ItemFlag.ItemIsUserCheckable
        if index.column() == 2:
            config = self._rows[index.row()].config
            if self._effective_type(config) is bool:
                return flags | Qt.ItemFlag.ItemIsUserCheckable
            return flags | Qt.ItemFlag.ItemIsEditable
        return flags

    def setData(
        self,
        index: QModelIndex | QPersistentModelIndex,
        value: object,
        role: int = Qt.ItemDataRole.EditRole,
    ) -> bool:
        if not index.isValid():
            return False
        row = index.row()
        col = index.column()
        if row < 0 or row >= len(self._rows):
            return False
        config = self._rows[row].config

        if col == 0 and role in (Qt.ItemDataRole.CheckStateRole, Qt.ItemDataRole.EditRole):
            # probably the CheckStateRole is enough.
            if role == Qt.ItemDataRole.EditRole and isinstance(value, bool):
                active = value
            else:
                active = Qt.CheckState(value) == Qt.CheckState.Checked
            config.active_override = active
            config.source = ParameterSource.CONFIG if active else ParameterSource.DEFAULT
            self._refresh_row(row)
            self.parameter_active_changed.emit(config.name, active, config.value)
            return True

        if col == 2 and role == Qt.ItemDataRole.CheckStateRole and self._effective_type(config) is bool:
            config.value = Qt.CheckState(value) == Qt.CheckState.Checked
            config.source = ParameterSource.CONFIG
            config.active_override = True
            self._refresh_row(row)
            self.parameter_value_changed.emit(config.name, config.value)
            return True

        if col == 2 and role == Qt.ItemDataRole.EditRole:
            config.value = value
            config.source = ParameterSource.CONFIG
            config.active_override = True
            self._refresh_row(row)
            self.parameter_value_changed.emit(config.name, value)
            return True

        return False

    def sort(self, column: int, order: Qt.SortOrder = Qt.SortOrder.AscendingOrder) -> None:
        if column not in (0, 1):
            return
        reverse = order == Qt.SortOrder.DescendingOrder
        self.layoutAboutToBeChanged.emit()
        if column == 1:
            self._rows.sort(key=lambda row: row.config.name, reverse=reverse)
        else:
            self._rows.sort(key=lambda row: row.order, reverse=reverse)
        self.layoutChanged.emit()

    @staticmethod
    def _effective_type(config: ParameterConfig) -> Any:
        if config.status in (ParameterSchemaStatus.UNKNOWN, ParameterSchemaStatus.DEPRECATED):
            return Any
        return config.type

    @staticmethod
    def _format_value(value: Any) -> str:
        if value is None:
            return ''
        return str(value)

    @staticmethod
    def _format_type(type_hint: Any) -> str:
        if type_hint is Any:
            return 'Any'
        if isinstance(type_hint, type):
            return type_hint.__name__
        return str(type_hint)

    def _data_for_column(self, config: ParameterConfig, col: int, role: int) -> object:
        if col == 0:
            return None
        if col == 1:
            return config.name
        if col == 2:
            return config.value if role == Qt.ItemDataRole.EditRole else self._format_value(config.value)
        if col == 3:
            return self._format_type(self._effective_type(config))
        if col == 4:
            return config.source.value if isinstance(config.source, ParameterSource) else str(config.source)
        if col == 5:
            return config.status.value if isinstance(config.status, ParameterSchemaStatus) else str(config.status)
        return None

    def _refresh_row(self, row: int) -> None:
        if not (0 <= row < len(self._rows)):
            return
        first = self.index(row, 0)
        last = self.index(row, len(_COLUMNS) - 1)
        self.dataChanged.emit(first, last, self._all_row_roles)


class PathEditor(QWidget):
    """Inline editor combining a line edit with a browse button."""

    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self._line_edit = QLineEdit()
        self._browse_button = QToolButton()
        self._browse_button.setText('Browse')
        self._browse_button.clicked.connect(self._browse)

        layout = QHBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self._line_edit)
        layout.addWidget(self._browse_button)

    def value(self) -> str:
        """Return the currently selected path string."""

        return self._line_edit.text()

    def set_value(self, value: Any) -> None:
        """Update the displayed path string."""

        self._line_edit.setText('' if value is None else str(value))

    def _browse(self) -> None:
        file_path, _ = QFileDialog.getOpenFileName(self, 'Select path', self._line_edit.text())
        if file_path:
            self._line_edit.setText(str(Path(file_path)))


class ParameterDelegate(QStyledItemDelegate):
    """Delegate that picks editors based on parameter type."""

    def createEditor(
        self,
        parent: QWidget,
        option: QStyleOptionViewItem,
        index: QModelIndex | QPersistentModelIndex,
    ) -> QWidget:
        param_type = index.data(_ParameterRoles.TYPE)
        if param_type is int:
            spin_box = QSpinBox(parent)
            spin_box.setRange(*INT_RANGE)
            return spin_box
        if param_type is float:
            double_box = QDoubleSpinBox(parent)
            double_box.setRange(*FLOAT_RANGE)
            double_box.setDecimals(FLOAT_DECIMALS)
            return double_box
        if param_type is bool:
            # this is not used any more. it is left just for legacy reason and can be removed, since boolean
            # values are now directly rendered with a checkbox without going through the delegate.
            checkbox = QCheckBox(parent)
            checkbox.toggled.connect(self._commit_checkbox)
            return checkbox
        if param_type is Path:
            return PathEditor(parent)
        return QLineEdit(parent)

    def setEditorData(
        self,
        editor: QWidget,
        index: QModelIndex | QPersistentModelIndex,
    ) -> None:
        value = index.data(Qt.ItemDataRole.EditRole)
        if isinstance(editor, QSpinBox):
            editor.setValue(int(value) if value is not None else 0)
            return
        if isinstance(editor, QDoubleSpinBox):
            editor.setValue(float(value) if value is not None else 0.0)
            return
        if isinstance(editor, QCheckBox):
            editor.setChecked(bool(value))
            return
        if isinstance(editor, PathEditor):
            editor.set_value(value)
            return
        if isinstance(editor, QLineEdit):
            editor.setText('' if value is None else str(value))
            return
        super().setEditorData(editor, index)

    def setModelData(
        self,
        editor: QWidget,
        model: QAbstractItemModel,
        index: QModelIndex | QPersistentModelIndex,
    ) -> None:
        if isinstance(editor, QSpinBox):
            model.setData(index, editor.value(), Qt.ItemDataRole.EditRole)
            return
        if isinstance(editor, QDoubleSpinBox):
            model.setData(index, editor.value(), Qt.ItemDataRole.EditRole)
            return
        if isinstance(editor, QCheckBox):
            model.setData(index, editor.isChecked(), Qt.ItemDataRole.EditRole)
            return
        if isinstance(editor, PathEditor):
            model.setData(index, editor.value(), Qt.ItemDataRole.EditRole)
            return
        if isinstance(editor, QLineEdit):
            model.setData(index, editor.text(), Qt.ItemDataRole.EditRole)
            return
        super().setModelData(editor, model, index)

    def _commit_checkbox(self, _: bool) -> None:
        editor = self.sender()
        if isinstance(editor, QWidget):
            self.commitData.emit(editor)
            self.closeEditor.emit(editor)


class ProcessorParameterEditor(QWidget):
    """Widget used to edit parameters and metadata of a processor entry."""

    replica_name_changed = Signal(str)
    new_only_changed = Signal(object)
    inheritance_changed = Signal(object)
    parameter_value_changed = Signal(str, object)
    parameter_active_changed = Signal(str, bool, object)
    reset_defaults_requested = Signal()
    remove_requested = Signal()
    edit_filters_requested = Signal()

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

        self._processor_label = QLabel('Processor: -')
        self._replica_edit = QLineEdit()
        self._new_only_checkbox = QCheckBox('only new items')
        self._inherit_checkbox = QCheckBox('inherit from base processor')
        self._new_only_checkbox.setTristate(True)
        self._inherit_checkbox.setTristate(True)

        self._table_model = ParameterTableModel(self)
        self._table = QTableView()
        self._table.setModel(self._table_model)
        self._table.setItemDelegateForColumn(2, ParameterDelegate(self._table))
        self._table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
        self._table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
        self._table.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked)
        self._table.setSortingEnabled(True)
        header = self._table.horizontalHeader()
        header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
        header.setStretchLastSection(False)
        header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
        header.setSectionsClickable(True)
        self._table.verticalHeader().setVisible(False)

        self._reset_button = QPushButton('Reset defaults')
        self._remove_button = QPushButton('Remove processor')
        self._edit_filters_button = QPushButton('Edit filters')

        self._setup_layout()
        self._wire_signals()

    def set_processor_data(self, config: ProcessorConfig) -> None:
        """Populate the editor with processor configuration data."""

        self._current_name = config.name
        base_name, replica = parse_processor_name(config.name)
        self._current_replica = replica

        with block_signals(self._replica_edit, self._new_only_checkbox, self._inherit_checkbox):
            self._processor_label.setText(f'Processor: {base_name}')
            self._replica_edit.setText(replica or '')
            self._new_only_checkbox.setCheckState(self._encode_tristate(config.new_only))
            self._inherit_checkbox.setEnabled(replica is not None)
            self._inherit_checkbox.setCheckState(self._encode_tristate(config.inheritance))

        parameters = list(config.parameters.values())
        self._table_model.set_parameters(parameters)

    def current_name(self) -> str | None:
        """Return the currently displayed processor full name."""

        return self._current_name

    def _setup_layout(self) -> None:
        form_layout = QFormLayout()
        form_layout.addRow('Replica:', self._replica_edit)

        toggle_layout = QHBoxLayout()
        toggle_layout.addWidget(self._new_only_checkbox)
        toggle_layout.addWidget(self._inherit_checkbox)
        toggle_layout.addStretch(1)

        button_layout = QHBoxLayout()
        button_layout.addWidget(self._reset_button)
        button_layout.addWidget(self._remove_button)
        button_layout.addWidget(self._edit_filters_button)
        button_layout.addStretch(1)

        layout = QVBoxLayout(self)
        layout.addWidget(self._processor_label)
        layout.addLayout(form_layout)
        layout.addLayout(toggle_layout)
        layout.addWidget(self._table)
        layout.addLayout(button_layout)

    def _wire_signals(self) -> None:
        self._replica_edit.editingFinished.connect(self._on_replica_changed)
        self._new_only_checkbox.stateChanged.connect(self._on_new_only_state_changed)
        self._inherit_checkbox.stateChanged.connect(self._on_inheritance_state_changed)
        self._table_model.parameter_value_changed.connect(self.parameter_value_changed)
        self._table_model.parameter_active_changed.connect(self.parameter_active_changed)
        self._reset_button.clicked.connect(self.reset_defaults_requested)
        self._remove_button.clicked.connect(self.remove_requested)
        self._edit_filters_button.clicked.connect(self.edit_filters_requested)

    def _on_replica_changed(self) -> None:
        new_replica = self._replica_edit.text().strip() or None
        if new_replica == self._current_replica:
            return
        self._current_replica = new_replica
        self.replica_name_changed.emit(new_replica or '')

    @staticmethod
    def _resolve_tristate(state: int) -> bool | None:
        if Qt.CheckState(state) == Qt.CheckState.Checked:
            value = True
        elif Qt.CheckState(state) == Qt.CheckState.Unchecked:
            value = False
        else:
            value = None
        return value

    def _on_new_only_state_changed(self, state: int) -> None:
        self.new_only_changed.emit(self._resolve_tristate(state))

    def _on_inheritance_state_changed(self, state: int) -> None:
        self.inheritance_changed.emit(self._resolve_tristate(state))

    @staticmethod
    def _encode_tristate(value: bool | None) -> Qt.CheckState:
        if value:
            return Qt.CheckState.Checked
        if value is False:
            return Qt.CheckState.Unchecked
        return Qt.CheckState.PartiallyChecked
