#  Copyright 2026 European Union
#  Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
#  SPDX-License-Identifier: EUPL-1.2
"""Group editor widget for managing processor groups.

:Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
:Description: Provide a table-based editor to rename groups, set descriptions, and reorder
    processors or nested groups without direct controller access.
"""

from __future__ import annotations

from PySide6.QtCore import QItemSelectionModel, QModelIndex, Qt, Signal
from PySide6.QtGui import QKeyEvent
from PySide6.QtWidgets import (
    QAbstractItemView,
    QFormLayout,
    QFrame,
    QHBoxLayout,
    QHeaderView,
    QLabel,
    QLineEdit,
    QPushButton,
    QStyle,
    QTableView,
    QToolButton,
    QVBoxLayout,
    QWidget,
)

from mafw.steering.models import GroupConfig
from mafw.steering_gui.models import PipelineItem, SteeringTreeModel, SteeringTreeRoles
from mafw.steering_gui.utils import block_signals


class _GroupTableView(QTableView):
    """Internal table view that exposes delete key handling."""

    delete_pressed = Signal()

    def keyPressEvent(self, event: QKeyEvent) -> None:
        if event.key() in (Qt.Key.Key_Delete, Qt.Key.Key_Backspace):
            self.delete_pressed.emit()
            event.accept()
            return
        super().keyPressEvent(event)


_DEFAULT_GROUP_NAME = 'Group'
"""Base name used when presenting an unnamed group."""


_DEFAULT_DESCRIPTION = ''
"""Fallback description used when none is configured."""


class GroupEditor(QWidget):
    """Widget used to edit group metadata and the processor list."""

    name_changed = Signal(str)
    description_changed = Signal(str)
    add_processor_requested = Signal()
    add_group_requested = Signal()
    edit_requested = Signal(object)
    remove_requested = Signal(list)
    move_requested = Signal(str, list)

    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self._model: SteeringTreeModel | None = None
        self._selection_model: QItemSelectionModel | None = None
        self._current_group: PipelineItem | None = None
        self._current_name: str | None = None
        self._current_description: str | None = None

        self._name_edit = QLineEdit()
        self._description_edit = QLineEdit()
        self._name_edit.setPlaceholderText('Group name')
        self._description_edit.setPlaceholderText('Optional description')

        self._table = _GroupTableView()
        self._table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
        self._table.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
        self._table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
        self._table.setDragEnabled(True)
        self._table.setAcceptDrops(True)
        self._table.setDropIndicatorShown(True)
        self._table.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
        self._table.verticalHeader().setVisible(False)
        self._table.horizontalHeader().setVisible(False)
        header = self._table.horizontalHeader()
        header.setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
        header.setStretchLastSection(True)

        self._move_up_button = QToolButton()
        self._move_down_button = QToolButton()
        self._edit_button = QToolButton()
        self._remove_button = QToolButton()
        self._apply_tool_icons()

        self._add_processor_button = QPushButton('Add processor')
        self._add_group_button = QPushButton('Add group')
        self._add_processor_button.setToolTip('Add a processor to this group')
        self._add_group_button.setToolTip('Add a nested group to this group')

        self._setup_layout()
        self._wire_signals()
        self._update_action_state()

    def set_model(self, model: SteeringTreeModel) -> None:
        """Attach the steering tree model to the group editor."""

        if self._model is not None:
            self._disconnect_model_signals(self._model)
        if self._selection_model is not None:
            try:
                self._selection_model.selectionChanged.disconnect(self._update_action_state)
            except (RuntimeError, TypeError):
                pass
        self._model = model
        self._table.setModel(model)
        self._connect_model_signals(model)
        self._selection_model = self._table.selectionModel()
        if self._selection_model is not None:
            self._selection_model.selectionChanged.connect(self._update_action_state)
        self._update_action_state()

    def set_group_item(self, item: PipelineItem) -> None:
        """Display the provided group item and its processors."""

        if not item.is_group() or self._model is None:
            return
        self._current_group = item
        config = item.config
        group_name = config.name if isinstance(config, GroupConfig) else _DEFAULT_GROUP_NAME
        description = config.description if isinstance(config, GroupConfig) else None
        self._current_name = group_name
        self._current_description = description

        with block_signals(self._name_edit, self._description_edit):
            self._name_edit.setText(group_name or _DEFAULT_GROUP_NAME)
            self._description_edit.setText(description or _DEFAULT_DESCRIPTION)

        self._set_root_index(item)
        self._update_action_state()

    def refresh(self) -> None:
        """Refresh the table root index after model updates."""

        if self._current_group is None or self._model is None:
            return
        self._set_root_index(self._current_group)
        self._update_action_state()

    def _apply_tool_icons(self) -> None:
        style = self.style()
        self._move_up_button.setIcon(style.standardIcon(QStyle.StandardPixmap.SP_ArrowUp))
        self._move_down_button.setIcon(style.standardIcon(QStyle.StandardPixmap.SP_ArrowDown))
        self._edit_button.setIcon(style.standardIcon(QStyle.StandardPixmap.SP_FileDialogDetailedView))
        self._remove_button.setIcon(style.standardIcon(QStyle.StandardPixmap.SP_TrashIcon))
        self._move_up_button.setToolTip('Move up')
        self._move_down_button.setToolTip('Move down')
        self._edit_button.setToolTip('Edit')
        self._remove_button.setToolTip('Delete')

    def _setup_layout(self) -> None:
        form = QFormLayout()
        form.addRow('Name:', self._name_edit)
        form.addRow('Description:', self._description_edit)

        header = QLabel('Processors:')
        separator = QFrame()
        separator.setFrameShape(QFrame.Shape.HLine)
        separator.setFrameShadow(QFrame.Shadow.Sunken)

        toolbar = QHBoxLayout()
        toolbar.addWidget(self._move_up_button)
        toolbar.addWidget(self._move_down_button)
        toolbar.addWidget(self._edit_button)
        toolbar.addWidget(self._remove_button)
        toolbar.addStretch(1)

        add_row = QHBoxLayout()
        add_row.addWidget(self._add_processor_button)
        add_row.addWidget(self._add_group_button)
        add_row.addStretch(1)

        layout = QVBoxLayout(self)
        layout.addLayout(form)
        layout.addWidget(header)
        layout.addWidget(separator)
        layout.addWidget(self._table)
        layout.addLayout(toolbar)
        layout.addLayout(add_row)

    def _wire_signals(self) -> None:
        self._name_edit.editingFinished.connect(self._on_name_changed)
        self._description_edit.editingFinished.connect(self._on_description_changed)
        self._table.doubleClicked.connect(self._on_edit_selected)
        self._table.delete_pressed.connect(self._on_remove_selected)
        self._move_up_button.clicked.connect(self._on_move_up)
        self._move_down_button.clicked.connect(self._on_move_down)
        self._edit_button.clicked.connect(self._on_edit_selected)
        self._remove_button.clicked.connect(self._on_remove_selected)
        self._add_processor_button.clicked.connect(self.add_processor_requested.emit)
        self._add_group_button.clicked.connect(self.add_group_requested.emit)

    def _connect_model_signals(self, model: SteeringTreeModel) -> None:
        model.modelReset.connect(self.refresh)
        model.layoutChanged.connect(self.refresh)

    def _disconnect_model_signals(self, model: SteeringTreeModel) -> None:
        for signal_name in ('modelReset', 'layoutChanged'):
            signal = getattr(model, signal_name)
            try:
                signal.disconnect(self.refresh)
            except (RuntimeError, TypeError):
                pass

    def _set_root_index(self, group_item: PipelineItem) -> None:
        if self._model is None:
            self._table.setRootIndex(QModelIndex())
            return
        group_index = self._find_item_index(group_item)
        if group_index.isValid():
            self._table.setRootIndex(group_index)
        else:
            self._table.setRootIndex(QModelIndex())

    def _find_item_index(self, target: PipelineItem) -> QModelIndex:
        if self._model is None:
            return QModelIndex()
        root_index = self._model.index(0, 0, QModelIndex())
        for row in range(self._model.rowCount(root_index)):
            candidate = self._model.index(row, 0, root_index)
            found = self._find_item_index_recursive(candidate, target)
            if found.isValid():
                return found
        return QModelIndex()

    def _find_item_index_recursive(self, parent: QModelIndex, target: PipelineItem) -> QModelIndex:
        if self._model is None:
            return QModelIndex()
        item = self._model.pipeline_item_from_index(parent)
        if item is target:
            return parent
        for row in range(self._model.rowCount(parent)):
            child = self._model.index(row, 0, parent)
            found = self._find_item_index_recursive(child, target)
            if found.isValid():
                return found
        return QModelIndex()

    def _selected_items(self) -> list[PipelineItem]:
        if self._model is None or self._selection_model is None:
            return []
        indexes = self._selection_model.selectedRows()
        items: list[PipelineItem] = []
        seen_ids: set[int] = set()
        for index in sorted(indexes, key=lambda idx: idx.row()):
            value = self._model.data(index, SteeringTreeRoles.PIPELINE_ITEM)
            if isinstance(value, PipelineItem):
                item_id = id(value)
                if item_id in seen_ids:
                    continue
                seen_ids.add(item_id)
                items.append(value)
        return items

    def _update_action_state(self, *_: object) -> None:
        has_selection = bool(self._selected_items())
        for button in (self._move_up_button, self._move_down_button, self._edit_button, self._remove_button):
            button.setEnabled(has_selection)

    def _on_name_changed(self) -> None:
        new_name = self._name_edit.text().strip()
        if new_name == (self._current_name or ''):
            return
        self._current_name = new_name
        self.name_changed.emit(new_name)

    def _on_description_changed(self) -> None:
        new_description = self._description_edit.text().strip()
        if new_description == (self._current_description or ''):
            return
        self._current_description = new_description
        self.description_changed.emit(new_description)

    def _on_edit_selected(self, *_: object) -> None:
        items = self._selected_items()
        if not items:
            return
        self.edit_requested.emit(items[0])

    def _on_remove_selected(self) -> None:
        items = self._selected_items()
        if not items:
            return
        self.remove_requested.emit(items)

    def _on_move_up(self) -> None:
        self._emit_move('up')

    def _on_move_down(self) -> None:
        self._emit_move('down')

    def _emit_move(self, direction: str) -> None:
        items = self._selected_items()
        if not items:
            return
        self.move_requested.emit(direction, items)
