from __future__ import annotations

from typing import Any

from .host_api import resolve_qevent_type, resolve_qt_constant

try:
    from qgis.PyQt.QtCore import QEvent, Qt
    from qgis.PyQt.QtGui import QKeySequence, QShortcut
    from qgis.PyQt.QtWidgets import (
        QCheckBox,
        QDockWidget,
        QHBoxLayout,
        QLabel,
        QLineEdit,
        QListWidget,
        QListWidgetItem,
        QPushButton,
        QScrollArea,
        QVBoxLayout,
        QWidget,
    )
except Exception:  # pragma: no cover
    class _QtShim:  # type: ignore[override]
        CustomContextMenu = 0
        PointingHandCursor = 0
        StrongFocus = 0
        Key_Return = 0
        Key_Enter = 0
        Key_Space = 0

    Qt = _QtShim()  # type: ignore[assignment]

    class _QEventShim:  # type: ignore[override]
        FocusIn = 8
        MouseButtonRelease = 3
        KeyPress = 6

    QEvent = _QEventShim()  # type: ignore[assignment]

    class _DummySignal:  # type: ignore[override]
        def connect(self, *_args, **_kwargs):
            return None

    class QKeySequence:  # type: ignore[override]
        def __init__(self, *_args, **_kwargs):
            pass

    class QShortcut:  # type: ignore[override]
        def __init__(self, *_args, **_kwargs):
            self.activated = _DummySignal()

    class _DummyWidget:  # type: ignore[override]
        def __init__(self, *_args, **_kwargs):
            pass

        def eventFilter(self, *_args, **_kwargs):
            return False

        def installEventFilter(self, *_args, **_kwargs):
            return None

    class QLabel(_DummyWidget):  # type: ignore[override]
        def setText(self, *_args, **_kwargs):
            return None

        def setToolTip(self, *_args, **_kwargs):
            return None

        def setStyleSheet(self, *_args, **_kwargs):
            return None

        def setCursor(self, *_args, **_kwargs):
            return None

        def setFocusPolicy(self, *_args, **_kwargs):
            return None

    class QPushButton(_DummyWidget):  # type: ignore[override]
        def __init__(self, *_args, **_kwargs):
            self.clicked = _DummySignal()

        def setFlat(self, *_args, **_kwargs):
            return None

        def setCursor(self, *_args, **_kwargs):
            return None

        def setStyleSheet(self, *_args, **_kwargs):
            return None

        def setToolTip(self, *_args, **_kwargs):
            return None

    class QCheckBox(_DummyWidget):  # type: ignore[override]
        def __init__(self, *_args, **_kwargs):
            self.stateChanged = _DummySignal()

        def isChecked(self):
            return True

        def setChecked(self, *_args, **_kwargs):
            return None

    class QLineEdit(_DummyWidget):  # type: ignore[override]
        def __init__(self, *_args, **_kwargs):
            self.textChanged = _DummySignal()
            self.returnPressed = _DummySignal()

        def text(self):
            return ""

        def setPlaceholderText(self, *_args, **_kwargs):
            return None

        def setFocus(self, *_args, **_kwargs):
            return None

        def selectAll(self, *_args, **_kwargs):
            return None

        def clear(self, *_args, **_kwargs):
            return None

        def setText(self, *_args, **_kwargs):
            return None

    class QVBoxLayout(_DummyWidget):  # type: ignore[override]
        def addWidget(self, *_args, **_kwargs):
            return None

        def setContentsMargins(self, *_args, **_kwargs):
            return None

    class QHBoxLayout(QVBoxLayout):  # type: ignore[override]
        pass

    class QListWidget(_DummyWidget):  # type: ignore[override]
        def __init__(self, *_args, **_kwargs):
            self.itemDoubleClicked = _DummySignal()
            self.customContextMenuRequested = _DummySignal()

        def clear(self):
            return None

        def addItem(self, *_args, **_kwargs):
            return None

        def row(self, *_args, **_kwargs):
            return -1

        def currentRow(self):
            return -1

        def setCurrentRow(self, *_args, **_kwargs):
            return None

        def setFocus(self, *_args, **_kwargs):
            return None

        def count(self):
            return 0

        def setContextMenuPolicy(self, *_args, **_kwargs):
            return None

        def mapToGlobal(self, pos):
            return pos

    class QListWidgetItem(_DummyWidget):  # type: ignore[override]
        pass

    class QWidget(_DummyWidget):  # type: ignore[override]
        def setLayout(self, *_args, **_kwargs):
            return None

    class QScrollArea(_DummyWidget):  # type: ignore[override]
        def setWidgetResizable(self, *_args, **_kwargs):
            return None

        def setWidget(self, *_args, **_kwargs):
            return None

    class QDockWidget(_DummyWidget):  # type: ignore[override]
        def setObjectName(self, *_args, **_kwargs):
            return None

        def setWidget(self, *_args, **_kwargs):
            return None


from .settings import AVAILABLE_LABEL_STYLE, LOCKED_LABEL_STYLE, status_style, tier_style


EVENT_TYPE_FOCUS_IN = resolve_qevent_type(QEvent, "FocusIn", 8)
EVENT_TYPE_MOUSE_BUTTON_RELEASE = resolve_qevent_type(QEvent, "MouseButtonRelease", 3)
EVENT_TYPE_KEY_PRESS = resolve_qevent_type(QEvent, "KeyPress", 6)
CONTEXT_MENU_POLICY_CUSTOM = resolve_qt_constant(Qt, "CustomContextMenu", "ContextMenuPolicy", 0)
KEY_RETURN = resolve_qt_constant(Qt, "Key_Return", "Key", 0)
KEY_ENTER = resolve_qt_constant(Qt, "Key_Enter", "Key", 0)
KEY_SPACE = resolve_qt_constant(Qt, "Key_Space", "Key", 0)


class WhiteboxDockPanel(QDockWidget):
    def __init__(self, parent=None):
        super().__init__("Whitebox Workflows", parent)
        self.setObjectName("WhiteboxWorkflowsDock")

        scroll_area = QScrollArea(self)
        if hasattr(scroll_area, "setWidgetResizable"):
            scroll_area.setWidgetResizable(True)

        container = QWidget(scroll_area)
        layout = QVBoxLayout(container)

        self._status_label = QLabel("Status: unknown")
        self._session_banner_label = QLabel("Session: tier=unknown | visible=0 | refreshed=never")
        self._session_banner_action_button = QPushButton("Open diagnostics")
        if hasattr(self._session_banner_action_button, "setFlat"):
            self._session_banner_action_button.setFlat(True)
        if hasattr(self._session_banner_action_button, "setCursor") and hasattr(Qt, "PointingHandCursor"):
            self._session_banner_action_button.setCursor(Qt.PointingHandCursor)
        if hasattr(self._session_banner_action_button, "setStyleSheet"):
            self._session_banner_action_button.setStyleSheet("text-align: left; color: #0D47A1; text-decoration: underline;")
        if hasattr(self._session_banner_action_button, "setToolTip"):
            self._session_banner_action_button.setToolTip("Open full runtime diagnostics")

        self._session_banner_row = QWidget(self)
        self._session_banner_row_layout = QHBoxLayout(self._session_banner_row)
        if hasattr(self._session_banner_row_layout, "setContentsMargins"):
            self._session_banner_row_layout.setContentsMargins(0, 0, 0, 0)
        self._session_banner_row_layout.addWidget(self._session_banner_label)
        self._session_banner_row_layout.addWidget(self._session_banner_action_button)
        self._session_banner_row.setLayout(self._session_banner_row_layout)

        if hasattr(self._session_banner_label, "setCursor") and hasattr(Qt, "PointingHandCursor"):
            self._session_banner_label.setCursor(Qt.PointingHandCursor)
        if hasattr(self._session_banner_label, "setWordWrap"):
            self._session_banner_label.setWordWrap(True)
        if hasattr(self._session_banner_label, "setFocusPolicy") and hasattr(Qt, "StrongFocus"):
            self._session_banner_label.setFocusPolicy(Qt.StrongFocus)
        self._tier_label = QLabel("Tier: unknown")
        self._catalog_label = QLabel("Catalog: unknown")
        self._version_label = QLabel("QGIS: unknown")
        self._search_label = QLabel("Tool Search")
        self._search_box = QLineEdit()
        self._search_box.setPlaceholderText("Search by tool id, name, category, or summary")
        self._quick_open_checkbox = QCheckBox("Quick-open top match on Enter")
        self._quick_open_checkbox.setChecked(True)
        self._show_available_checkbox = QCheckBox("Show available")
        self._show_available_checkbox.setChecked(True)
        self._show_locked_checkbox = QCheckBox("Show locked")
        self._show_locked_checkbox.setChecked(True)
        self._matches_label = QLabel("Matches: 0")
        self._results_list = QListWidget()

        self._favorites_label = QLabel("Favorite Tools")
        self._favorites_list = QListWidget()
        self._favorite_add_button = QPushButton("Add Favorite")
        self._favorite_remove_button = QPushButton("Remove Favorite")
        self._favorite_up_button = QPushButton("Move Up")
        self._favorite_down_button = QPushButton("Move Down")
        self._favorite_clear_button = QPushButton("Clear Favorites")

        self._recent_label = QLabel("Recent Tools")
        self._recent_list = QListWidget()
        self._recent_clear_button = QPushButton("Clear Recents")

        self._recipes_label = QLabel("Workflow Recipes")
        self._recipes_show_locked_checkbox = QCheckBox("Include locked recipes")
        self._recipes_show_locked_checkbox.setChecked(True)
        self._recipes_list = QListWidget()
        self._recipes_open_button = QPushButton("Open Recipe")
        self._recipes_copy_steps_button = QPushButton("Copy Recipe Steps")
        self._recipes_upgrade_button = QPushButton("Why Is This Locked?")
        self._recipes_open_file_button = QPushButton("Open Recipe File")
        self._recipes_reload_button = QPushButton("Reload Recipe File")
        self._recipes_validate_button = QPushButton("Validate Recipe File")
        self._recipe_preview_label = QLabel("Recipe details: select a recipe to preview steps.")
        if hasattr(self._recipe_preview_label, "setWordWrap"):
            self._recipe_preview_label.setWordWrap(True)
        self._shortcut_hint_label = QLabel(
            "Shortcuts: /=focus search, Esc=clear search, Up/Down=jump to results, Enter=open result, Ctrl/Cmd+D=toggle favorite, Delete/Backspace=remove favorite, Ctrl/Cmd+Shift+D=diagnostics"
        )
        if hasattr(self._shortcut_hint_label, "setWordWrap"):
            self._shortcut_hint_label.setWordWrap(True)

        self._refresh_button = QPushButton("Refresh Catalog")
        self._diagnostics_button = QPushButton("Runtime Diagnostics")

        if hasattr(self._favorite_add_button, "setToolTip"):
            self._favorite_add_button.setToolTip("Add selected result to favorites")
        if hasattr(self._favorite_remove_button, "setToolTip"):
            self._favorite_remove_button.setToolTip("Remove selected favorite")
        if hasattr(self._favorite_up_button, "setToolTip"):
            self._favorite_up_button.setToolTip("Move selected favorite up")
        if hasattr(self._favorite_down_button, "setToolTip"):
            self._favorite_down_button.setToolTip("Move selected favorite down")

        layout.addWidget(self._status_label)
        layout.addWidget(self._session_banner_row)
        layout.addWidget(self._tier_label)
        layout.addWidget(self._catalog_label)
        layout.addWidget(self._version_label)
        layout.addWidget(self._search_label)
        layout.addWidget(self._search_box)
        layout.addWidget(self._quick_open_checkbox)
        layout.addWidget(self._show_available_checkbox)
        layout.addWidget(self._show_locked_checkbox)
        layout.addWidget(self._matches_label)
        layout.addWidget(self._results_list)
        layout.addWidget(self._favorites_label)
        layout.addWidget(self._favorites_list)
        layout.addWidget(self._favorite_add_button)
        layout.addWidget(self._favorite_remove_button)
        layout.addWidget(self._favorite_up_button)
        layout.addWidget(self._favorite_down_button)
        layout.addWidget(self._favorite_clear_button)
        layout.addWidget(self._recent_label)
        layout.addWidget(self._recent_list)
        layout.addWidget(self._recent_clear_button)
        layout.addWidget(self._recipes_label)
        layout.addWidget(self._recipes_show_locked_checkbox)
        layout.addWidget(self._recipes_list)
        layout.addWidget(self._recipes_open_button)
        layout.addWidget(self._recipes_copy_steps_button)
        layout.addWidget(self._recipes_upgrade_button)
        layout.addWidget(self._recipes_open_file_button)
        layout.addWidget(self._recipes_reload_button)
        layout.addWidget(self._recipes_validate_button)
        layout.addWidget(self._recipe_preview_label)
        layout.addWidget(self._shortcut_hint_label)
        layout.addWidget(self._refresh_button)
        layout.addWidget(self._diagnostics_button)

        container.setLayout(layout)
        if hasattr(scroll_area, "setWidget"):
            scroll_area.setWidget(container)
        self.setWidget(scroll_area)

        # Keep the dock compact-friendly; users can still grow it as needed.
        if hasattr(self, "setMinimumSize"):
            self.setMinimumSize(220, 200)

        self._catalog: list[dict[str, Any]] = []
        self._filtered_tool_ids: list[str] = []
        self._favorite_tool_ids: list[str] = []
        self._favorite_display_ids: list[str] = []
        self._recent_tool_ids: list[str] = []
        self._recipes: list[dict[str, Any]] = []
        self._recipe_ids: list[str] = []
        self._search_box.textChanged.connect(self._on_search_text_changed)
        self._search_box.returnPressed.connect(self._open_quick_match)
        self._show_available_checkbox.stateChanged.connect(self._on_filter_changed)
        self._show_locked_checkbox.stateChanged.connect(self._on_filter_changed)
        self._recipes_show_locked_checkbox.stateChanged.connect(self._on_recipe_discovery_changed)
        self._recipes_open_button.clicked.connect(self._open_selected_recipe)
        self._recipes_copy_steps_button.clicked.connect(self._copy_selected_recipe_steps)
        self._recipes_upgrade_button.clicked.connect(self._show_selected_recipe_upgrade)
        self._recipes_open_file_button.clicked.connect(self._open_recipe_file)
        self._recipes_reload_button.clicked.connect(self._reload_recipe_file)
        self._recipes_validate_button.clicked.connect(self._validate_recipe_file)
        row_changed = getattr(self._recipes_list, "currentRowChanged", None)
        if row_changed is not None:
            row_changed.connect(self._on_recipe_selection_changed)

        self._search_box.installEventFilter(self)
        self._results_list.installEventFilter(self)
        self._favorites_list.installEventFilter(self)
        self._recent_list.installEventFilter(self)
        self._session_banner_label.installEventFilter(self)

        self._results_list.setContextMenuPolicy(CONTEXT_MENU_POLICY_CUSTOM)
        self._results_list.customContextMenuRequested.connect(self._on_results_context_menu)
        self._favorites_list.setContextMenuPolicy(CONTEXT_MENU_POLICY_CUSTOM)
        self._favorites_list.customContextMenuRequested.connect(self._on_favorites_context_menu)

        self._open_tool_callback = None
        self._open_recipe_callback = None
        self._copy_recipe_steps_callback = None
        self._show_recipe_upgrade_callback = None
        self._open_recipe_file_callback = None
        self._reload_recipe_file_callback = None
        self._validate_recipe_file_callback = None
        self._tool_context_menu_callback = None

        # Keyboard accelerators for high-frequency workflows.
        self._shortcut_open_result = QShortcut(QKeySequence("Return"), self)
        self._shortcut_open_result_alt = QShortcut(QKeySequence("Enter"), self)
        self._shortcut_open_result_list = QShortcut(QKeySequence("Return"), self._results_list)
        self._shortcut_open_result_alt_list = QShortcut(QKeySequence("Enter"), self._results_list)
        self._shortcut_focus_search_slash = QShortcut(QKeySequence("/"), self)
        self._shortcut_focus_search_ctrlf = QShortcut(QKeySequence("Ctrl+F"), self)
        self._shortcut_focus_search_metaf = QShortcut(QKeySequence("Meta+F"), self)
        self._shortcut_focus_results_down = QShortcut(QKeySequence("Down"), self._search_box)
        self._shortcut_focus_results_up = QShortcut(QKeySequence("Up"), self._search_box)
        self._shortcut_clear_search_esc = QShortcut(QKeySequence("Esc"), self)
        self._shortcut_toggle_favorite_ctrl = QShortcut(QKeySequence("Ctrl+D"), self)
        self._shortcut_toggle_favorite_meta = QShortcut(QKeySequence("Meta+D"), self)
        self._shortcut_diagnostics_ctrl_shift = QShortcut(QKeySequence("Ctrl+Shift+D"), self)
        self._shortcut_diagnostics_meta_shift = QShortcut(QKeySequence("Meta+Shift+D"), self)
        self._shortcut_remove_favorite_del = QShortcut(QKeySequence("Delete"), self)
        self._shortcut_remove_favorite_back = QShortcut(QKeySequence("Backspace"), self)

        self._shortcut_open_result.activated.connect(self._open_selected_result)
        self._shortcut_open_result_alt.activated.connect(self._open_selected_result)
        self._shortcut_open_result_list.activated.connect(self._open_selected_result)
        self._shortcut_open_result_alt_list.activated.connect(self._open_selected_result)
        self._shortcut_focus_search_slash.activated.connect(self._focus_search_box)
        self._shortcut_focus_search_ctrlf.activated.connect(self._focus_search_box)
        self._shortcut_focus_search_metaf.activated.connect(self._focus_search_box)
        self._shortcut_focus_results_down.activated.connect(self._focus_results_first)
        self._shortcut_focus_results_up.activated.connect(self._focus_results_last)
        self._shortcut_clear_search_esc.activated.connect(self._clear_search_box)
        self._shortcut_toggle_favorite_ctrl.activated.connect(self._toggle_selected_favorite)
        self._shortcut_toggle_favorite_meta.activated.connect(self._toggle_selected_favorite)
        self._shortcut_diagnostics_ctrl_shift.activated.connect(self._trigger_diagnostics)
        self._shortcut_diagnostics_meta_shift.activated.connect(self._trigger_diagnostics)
        self._shortcut_remove_favorite_del.activated.connect(self._remove_selected_favorite_shortcut)
        self._shortcut_remove_favorite_back.activated.connect(self._remove_selected_favorite_shortcut)

        self._diagnostics_callback = None
        self._toggle_favorite_callback = None
        self._remove_favorite_shortcut_callback = None
        self._filter_state_callback = None
        self._search_state_callback = None
        self._recipe_discovery_state_callback = None
        self._focus_area_callback = None
        self._focus_area = "search"
        self._session_banner_click_callback = None

    def on_refresh(self, callback):
        self._refresh_button.clicked.connect(callback)

    def on_diagnostics(self, callback):
        self._diagnostics_callback = callback
        self._diagnostics_button.clicked.connect(self._trigger_diagnostics)
        self._session_banner_action_button.clicked.connect(self._trigger_diagnostics)

    def on_open_tool(self, callback):
        self._open_tool_callback = callback

        def _open(item):
            row = self._results_list.row(item)
            if row < 0 or row >= len(self._filtered_tool_ids):
                return
            callback(self._filtered_tool_ids[row])

        self._results_list.itemDoubleClicked.connect(_open)

    def on_open_recent_tool(self, callback):
        def _open_recent(item):
            row = self._recent_list.row(item)
            if row < 0 or row >= len(self._recent_tool_ids):
                return
            callback(self._recent_tool_ids[row])

        self._recent_list.itemDoubleClicked.connect(_open_recent)

    def on_open_favorite_tool(self, callback):
        def _open_favorite(item):
            row = self._favorites_list.row(item)
            if row < 0 or row >= len(self._favorite_display_ids):
                return
            callback(self._favorite_display_ids[row])

        self._favorites_list.itemDoubleClicked.connect(_open_favorite)

    def on_open_recipe(self, callback):
        self._open_recipe_callback = callback

        def _open_recipe(_item):
            recipe_id = self.selected_recipe_id()
            if recipe_id:
                callback(recipe_id)

        self._recipes_list.itemDoubleClicked.connect(_open_recipe)

    def on_show_recipe_upgrade(self, callback):
        self._show_recipe_upgrade_callback = callback

    def on_copy_recipe_steps(self, callback):
        self._copy_recipe_steps_callback = callback

    def on_open_recipe_file(self, callback):
        self._open_recipe_file_callback = callback

    def on_reload_recipe_file(self, callback):
        self._reload_recipe_file_callback = callback

    def on_validate_recipe_file(self, callback):
        self._validate_recipe_file_callback = callback

    def on_add_favorite(self, callback):
        self._favorite_add_button.clicked.connect(callback)

    def on_remove_favorite(self, callback):
        self._favorite_remove_button.clicked.connect(callback)

    def on_toggle_selected_favorite(self, callback):
        self._toggle_favorite_callback = callback

    def on_remove_selected_favorite_shortcut(self, callback):
        self._remove_favorite_shortcut_callback = callback

    def on_move_favorite_up(self, callback):
        self._favorite_up_button.clicked.connect(callback)

    def on_move_favorite_down(self, callback):
        self._favorite_down_button.clicked.connect(callback)

    def on_clear_favorites(self, callback):
        self._favorite_clear_button.clicked.connect(callback)

    def on_clear_recents(self, callback):
        self._recent_clear_button.clicked.connect(callback)

    def on_quick_open_toggled(self, callback):
        self._quick_open_checkbox.stateChanged.connect(callback)

    def on_filter_state_changed(self, callback):
        self._filter_state_callback = callback

    def on_search_state_changed(self, callback):
        self._search_state_callback = callback

    def on_recipe_discovery_state_changed(self, callback):
        self._recipe_discovery_state_callback = callback

    def on_focus_area_changed(self, callback):
        self._focus_area_callback = callback

    def on_session_banner_clicked(self, callback):
        self._session_banner_click_callback = callback

    def on_tool_context_menu(self, callback):
        self._tool_context_menu_callback = callback

    def _trigger_diagnostics(self):
        if self._diagnostics_callback is not None:
            self._diagnostics_callback()

    def update_state(
        self,
        *,
        status: str,
        fallback_tier: str,
        effective_tier: str,
        available_count: int,
        locked_count: int,
        qgis_version: str,
    ) -> None:
        self._status_label.setText(f"Status: {status}")
        if hasattr(self._status_label, "setStyleSheet"):
            self._status_label.setStyleSheet(status_style(status))
        self._tier_label.setText(
            f"Tier: fallback={fallback_tier}, effective={effective_tier}"
        )
        if hasattr(self._tier_label, "setStyleSheet"):
            self._tier_label.setStyleSheet(tier_style(effective_tier))
        avail_txt = f"Available: {available_count}"
        locked_txt = f"  Locked: {locked_count}"
        self._catalog_label.setText(f"Catalog: {avail_txt}{locked_txt}")
        if hasattr(self._catalog_label, "setStyleSheet"):
            if locked_count > 0 and available_count == 0:
                self._catalog_label.setStyleSheet(LOCKED_LABEL_STYLE)
            elif available_count > 0:
                self._catalog_label.setStyleSheet(AVAILABLE_LABEL_STYLE)
            else:
                self._catalog_label.setStyleSheet("")
        self._version_label.setText(f"QGIS: {qgis_version or 'unknown'}")

    def update_session_banner(
        self,
        *,
        status: str,
        effective_tier: str,
        visible_count: int,
        refreshed_at: str,
        detail: str = "",
    ) -> None:
        norm = str(status).strip().lower()
        if norm == "ok":
            status_text = "OK"
            style = "color: #1B5E20; text-decoration: underline;"
        elif norm == "bootstrap_error":
            status_text = "BOOTSTRAP_ERROR"
            style = "color: #E65100; text-decoration: underline;"
        else:
            status_text = "ERROR"
            style = "color: #B71C1C; text-decoration: underline;"

        self._session_banner_label.setText(
            f"Session: status={status_text} | tier={effective_tier} | visible={visible_count} | refreshed={refreshed_at}"
        )
        self._session_banner_label.setStyleSheet(style)

        msg = str(detail or "").strip()
        if msg:
            self._session_banner_label.setToolTip(f"Status detail: {msg}\nClick or press Enter/Space for full diagnostics")
        else:
            self._session_banner_label.setToolTip("Status detail: none\nClick or press Enter/Space for full diagnostics")

    def set_catalog(self, catalog: list[dict[str, Any]]) -> None:
        self._catalog = list(catalog)
        self._refresh_results(self._search_box.text())
        self._refresh_favorites_list()

    def set_favorites(self, tool_ids: list[str]) -> None:
        self._favorite_tool_ids = list(tool_ids)
        self._refresh_results(self._search_box.text())
        self._refresh_favorites_list()

    def selected_result_tool_id(self) -> str:
        row = self._results_list.currentRow()
        if row < 0 or row >= len(self._filtered_tool_ids):
            return ""
        return self._filtered_tool_ids[row]

    def select_result_by_tool_id(self, tool_id: str) -> None:
        try:
            idx = self._filtered_tool_ids.index(str(tool_id))
        except ValueError:
            return
        self._results_list.setCurrentRow(idx)

    def quick_open_enabled(self) -> bool:
        return bool(self._quick_open_checkbox.isChecked())

    def set_quick_open_enabled(self, enabled: bool) -> None:
        self._quick_open_checkbox.setChecked(bool(enabled))

    def show_available_enabled(self) -> bool:
        return bool(self._show_available_checkbox.isChecked())

    def set_show_available_enabled(self, enabled: bool) -> None:
        self._show_available_checkbox.setChecked(bool(enabled))

    def show_locked_enabled(self) -> bool:
        return bool(self._show_locked_checkbox.isChecked())

    def set_show_locked_enabled(self, enabled: bool) -> None:
        self._show_locked_checkbox.setChecked(bool(enabled))

    def show_locked_recipes_enabled(self) -> bool:
        return bool(self._recipes_show_locked_checkbox.isChecked())

    def set_show_locked_recipes_enabled(self, enabled: bool) -> None:
        self._recipes_show_locked_checkbox.setChecked(bool(enabled))

    def search_text(self) -> str:
        return str(self._search_box.text())

    def set_search_text(self, text: str) -> None:
        self._search_box.setText(str(text))

    def focus_area(self) -> str:
        return str(self._focus_area)

    def set_focus_area(self, area: str) -> None:
        target = str(area).strip().lower()
        if target == "results":
            self._focus_results_first()
            return
        if target == "favorites":
            self._favorites_list.setFocus()
            return
        if target == "recents":
            self._recent_list.setFocus()
            return
        self._focus_search_box()

    def eventFilter(self, obj, event):  # type: ignore[override]
        if event is not None and event.type() == EVENT_TYPE_MOUSE_BUTTON_RELEASE:
            if obj is self._session_banner_label and self._session_banner_click_callback is not None:
                self._session_banner_click_callback()
                return True

        if event is not None and event.type() == EVENT_TYPE_KEY_PRESS:
            if obj is self._session_banner_label and self._session_banner_click_callback is not None:
                key = event.key() if hasattr(event, "key") else None
                if key in (KEY_RETURN, KEY_ENTER, KEY_SPACE):
                    self._session_banner_click_callback()
                    return True

        if event is not None and event.type() == EVENT_TYPE_FOCUS_IN:
            area = self._focus_area_name_for_obj(obj)
            if area:
                self._focus_area = area
                if self._focus_area_callback is not None:
                    self._focus_area_callback()
        return super().eventFilter(obj, event)

    def _focus_area_name_for_obj(self, obj) -> str:
        if obj is self._search_box:
            return "search"
        if obj is self._results_list:
            return "results"
        if obj is self._favorites_list:
            return "favorites"
        if obj is self._recent_list:
            return "recents"
        return ""

    def top_result_tool_id(self) -> str:
        if not self._filtered_tool_ids:
            return ""
        return self._filtered_tool_ids[0]

    def selected_favorite_tool_id(self) -> str:
        row = self._favorites_list.currentRow()
        if row < 0 or row >= len(self._favorite_display_ids):
            return ""
        return self._favorite_display_ids[row]

    def select_favorite_by_tool_id(self, tool_id: str) -> None:
        try:
            idx = self._favorite_display_ids.index(str(tool_id))
        except ValueError:
            return
        self._favorites_list.setCurrentRow(idx)

    def selected_favorite_index(self) -> int:
        row = self._favorites_list.currentRow()
        if row < 0 or row >= len(self._favorite_display_ids):
            return -1
        return row

    def is_favorite(self, tool_id: str) -> bool:
        return tool_id in self._favorite_tool_ids

    def select_favorite_index(self, index: int) -> None:
        if index < 0 or index >= len(self._favorite_display_ids):
            return
        self._favorites_list.setCurrentRow(index)

    def set_recent_tools(self, tool_ids: list[str]) -> None:
        self._recent_tool_ids = list(tool_ids)
        self._recent_list.clear()
        for tool_id in self._recent_tool_ids:
            self._recent_list.addItem(QListWidgetItem(tool_id))

    def set_recipes(self, recipes: list[dict[str, Any]]) -> None:
        # Keep backing recipe rows aligned exactly with visible list items.
        # This avoids preview/selection mismatches when malformed entries are skipped.
        self._recipes = []
        self._recipe_ids = []
        self._recipes_list.clear()

        for recipe in [dict(r) for r in recipes]:
            recipe_id = str(recipe.get("id", "")).strip()
            if not recipe_id:
                continue
            title = str(recipe.get("title", recipe_id)).strip() or recipe_id
            tier = str(recipe.get("tier", "open")).strip().upper()
            summary = str(recipe.get("summary", "")).strip()
            is_locked = bool(recipe.get("locked", False))
            lock_prefix = "[LOCKED] " if is_locked else ""
            label = f"{lock_prefix}[{tier}] {title}"
            item = QListWidgetItem(label)
            lock_reason = str(recipe.get("locked_reason", "")).strip()
            tooltip_parts = []
            if summary:
                tooltip_parts.append(summary)
            if lock_reason:
                tooltip_parts.append(lock_reason)
            if tooltip_parts and hasattr(item, "setToolTip"):
                item.setToolTip("\n".join(tooltip_parts))
            if is_locked:
                try:
                    from qgis.PyQt.QtGui import QColor
                    item.setForeground(QColor("#F57F17"))
                except Exception:
                    pass
            self._recipes_list.addItem(item)
            self._recipes.append(recipe)
            self._recipe_ids.append(recipe_id)

        if self._recipe_ids:
            self._recipes_list.setCurrentRow(0)
            self._update_recipe_preview_by_row(0)
        else:
            self._recipe_preview_label.setText("Recipe details: no recipes available for this catalog.")
            if hasattr(self._recipes_upgrade_button, "setEnabled"):
                self._recipes_upgrade_button.setEnabled(False)

    def selected_recipe_id(self) -> str:
        row = self._recipes_list.currentRow()
        if row < 0 or row >= len(self._recipe_ids):
            return ""
        return self._recipe_ids[row]

    def _open_selected_recipe(self) -> None:
        if self._open_recipe_callback is None:
            return
        recipe_id = self.selected_recipe_id()
        if recipe_id:
            self._open_recipe_callback(recipe_id)

    def _show_selected_recipe_upgrade(self) -> None:
        if self._show_recipe_upgrade_callback is None:
            return
        recipe_id = self.selected_recipe_id()
        if recipe_id:
            self._show_recipe_upgrade_callback(recipe_id)

    def _copy_selected_recipe_steps(self) -> None:
        if self._copy_recipe_steps_callback is None:
            return
        recipe_id = self.selected_recipe_id()
        if recipe_id:
            self._copy_recipe_steps_callback(recipe_id)

    def _open_recipe_file(self) -> None:
        if self._open_recipe_file_callback is None:
            return
        self._open_recipe_file_callback()

    def _reload_recipe_file(self) -> None:
        if self._reload_recipe_file_callback is None:
            return
        self._reload_recipe_file_callback()

    def _validate_recipe_file(self) -> None:
        if self._validate_recipe_file_callback is None:
            return
        self._validate_recipe_file_callback()

    def _on_recipe_selection_changed(self, row: int) -> None:
        self._update_recipe_preview_by_row(int(row))

    def _update_recipe_preview_by_row(self, row: int) -> None:
        if row < 0 or row >= len(self._recipes):
            self._recipe_preview_label.setText("Recipe details: select a recipe to preview steps.")
            if hasattr(self._recipes_upgrade_button, "setEnabled"):
                self._recipes_upgrade_button.setEnabled(False)
            return

        recipe = self._recipes[row]
        title = str(recipe.get("title", recipe.get("id", "Recipe"))).strip() or "Recipe"
        summary = str(recipe.get("summary", "")).strip()
        input_hint = str(recipe.get("input_hint", "")).strip()
        output_hint = str(recipe.get("output_hint", "")).strip()
        tools = [str(t) for t in recipe.get("tools", []) if str(t).strip()]
        lock_reason = str(recipe.get("locked_reason", "")).strip()

        steps = ", ".join(tools) if tools else "(no steps)"
        parts = [f"{title}: {summary}" if summary else title, f"Steps: {steps}"]
        if input_hint:
            parts.append(f"Input hint: {input_hint}")
        if output_hint:
            parts.append(f"Output hint: {output_hint}")
        if lock_reason:
            parts.append(f"Status: {lock_reason}")
        self._recipe_preview_label.setText("\n".join(parts))

        is_locked = bool(recipe.get("locked", False))
        if hasattr(self._recipes_upgrade_button, "setEnabled"):
            self._recipes_upgrade_button.setEnabled(is_locked)

    def _refresh_favorites_list(self) -> None:
        self._favorites_list.clear()
        self._favorite_display_ids = []

        catalog_by_id = {str(item.get("id", "")): item for item in self._catalog}
        for tool_id in self._favorite_tool_ids:
            item = catalog_by_id.get(tool_id)
            if item is None:
                label = tool_id
            else:
                display_name = str(item.get("display_name", tool_id))
                is_locked = bool(item.get("locked", False))
                badge = "[LOCKED] " if is_locked else ""
                label = f"{badge}{display_name} ({tool_id})"
            self._favorites_list.addItem(QListWidgetItem(label))
            self._favorite_display_ids.append(tool_id)

    def _on_search_text_changed(self, text: str) -> None:
        self._refresh_results(text)
        if self._search_state_callback is not None:
            self._search_state_callback()

    def _on_filter_changed(self, _value: int) -> None:
        self._refresh_results(self._search_box.text())
        if self._filter_state_callback is not None:
            self._filter_state_callback()

    def _on_recipe_discovery_changed(self, _value: int) -> None:
        if self._recipe_discovery_state_callback is not None:
            self._recipe_discovery_state_callback()

    def _open_selected_result(self) -> None:
        callback = getattr(self, "_open_tool_callback", None)
        if callback is None:
            return
        tool_id = self.selected_result_tool_id()
        if tool_id:
            callback(tool_id)

    def _toggle_selected_favorite(self) -> None:
        if self._toggle_favorite_callback is None:
            return
        self._toggle_favorite_callback()

    def _remove_selected_favorite_shortcut(self) -> None:
        if self._remove_favorite_shortcut_callback is None:
            return
        self._remove_favorite_shortcut_callback()

    def _focus_search_box(self) -> None:
        self._focus_area = "search"
        self._search_box.setFocus()
        self._search_box.selectAll()

    def _clear_search_box(self) -> None:
        self._search_box.clear()

    def _focus_results_first(self) -> None:
        self._focus_results_index(0)

    def _focus_results_last(self) -> None:
        self._focus_results_index(len(self._filtered_tool_ids) - 1)

    def _focus_results_index(self, index: int) -> None:
        # Use filtered tool ids count to avoid selecting empty-state hint rows.
        if not self._filtered_tool_ids:
            return
        if index < 0:
            index = 0
        if index >= len(self._filtered_tool_ids):
            index = len(self._filtered_tool_ids) - 1
        self._focus_area = "results"
        self._results_list.setCurrentRow(index)
        self._results_list.setFocus()

    def _open_quick_match(self) -> None:
        if not self._quick_open_checkbox.isChecked():
            return
        callback = getattr(self, "_open_tool_callback", None)
        if callback is None:
            return
        tool_id = self.top_result_tool_id()
        if tool_id:
            callback(tool_id)

    def _on_results_context_menu(self, pos) -> None:
        if self._tool_context_menu_callback is None:
            return
        tool_id = self.selected_result_tool_id()
        if not tool_id:
            return
        self._tool_context_menu_callback(
            "results",
            tool_id,
            self._results_list.mapToGlobal(pos),
        )

    def _on_favorites_context_menu(self, pos) -> None:
        if self._tool_context_menu_callback is None:
            return
        tool_id = self.selected_favorite_tool_id()
        if not tool_id:
            return
        self._tool_context_menu_callback(
            "favorites",
            tool_id,
            self._favorites_list.mapToGlobal(pos),
        )

    def _refresh_results(self, text: str) -> None:
        query = text.strip().lower()
        show_discovery = bool(query)

        show_available = bool(self._show_available_checkbox.isChecked())
        show_locked = bool(self._show_locked_checkbox.isChecked())

        self._results_list.clear()
        self._filtered_tool_ids = []

        matches = 0
        for item in self._catalog:
            is_locked = bool(item.get("locked", False))
            if is_locked and not show_locked:
                continue
            if (not is_locked) and not show_available:
                continue

            tool_id = str(item.get("id", ""))

            # Keep default blank search focused: only show favorites and recents
            # until the user types a query.
            if not show_discovery and tool_id not in self._favorite_tool_ids and tool_id not in self._recent_tool_ids:
                continue

            default_visible = bool(item.get("display_default_visible", True))
            if not default_visible and not query and tool_id not in self._favorite_tool_ids:
                continue

            haystack = " ".join(
                [
                    tool_id,
                    str(item.get("display_name", "")),
                    str(item.get("category", "")),
                    str(item.get("summary", "")),
                ]
            ).lower()
            if query and query not in haystack:
                continue

            display_name = str(item.get("display_name", tool_id))
            category = str(item.get("category", "General"))
            badge = "[LOCKED] " if is_locked else ""
            star = "[FAV] " if tool_id in self._favorite_tool_ids else ""
            label = f"{star}{badge}{display_name} ({tool_id}) — {category}"

            list_item = QListWidgetItem(label)
            # Semantic colour: locked tools use amber, available tools use default.
            if is_locked:
                try:
                    from qgis.PyQt.QtGui import QColor
                    list_item.setForeground(QColor("#F57F17"))
                except Exception:
                    pass
            self._results_list.addItem(list_item)
            self._filtered_tool_ids.append(tool_id)
            matches += 1

        if matches == 0:
            hint = self._empty_state_message(
                query=query,
                show_available=show_available,
                show_locked=show_locked,
            )
            self._results_list.addItem(QListWidgetItem(hint))

        self._matches_label.setText(f"Matches: {matches}")

    def _empty_state_message(
        self,
        *,
        query: str,
        show_available: bool,
        show_locked: bool,
    ) -> str:
        if not show_available and not show_locked:
            return "No matches. Enable 'Show available' and/or 'Show locked'."
        if query:
            return "No matches. Try a broader search or adjust filters."
        return "No recent or favorite tools yet. Type in search to browse all tools."


def summarize_catalog(catalog: list[dict[str, Any]]) -> tuple[int, int]:
    available = 0
    locked = 0
    for item in catalog:
        if bool(item.get("locked", False)):
            locked += 1
        else:
            available += 1
    return available, locked
