# -*- coding: utf-8 -*-
"""
Click-Classify Dialog
=====================
Configuration dialog for the Click-Classify map tool.

Layout:
  ┌──────────────────────────────────────────────────────┐
  │  Layer:  [QgsMapLayerComboBox]                        │
  │  Count field:  [QgsFieldComboBox]  [+ Create new]    │
  │  Label field:  [QgsFieldComboBox]  ☐ (optional)      │
  ├──────────────────────────────────────────────────────┤
  │  Click schedule:                 [+ Add] [- Remove]  │
  │  ┌──────┬───────────┬──────────┬────────────────┐    │
  │  │ Step │   Value   │   Label  │     Colour      │    │
  │  ├──────┼───────────┼──────────┼────────────────┤    │
  │  │   0  │     0     │  (none)  │ ████ #aaaaaa   │    │
  │  │   1  │     1     │  Good    │ ████ #00aa00   │    │
  │  └──────┴───────────┴──────────┴────────────────┘    │
  ├──────────────────────────────────────────────────────┤
  │  Auto-save:  ⬤ Each click  ○ Manual  [Save Now]     │
  │  Status: ─────────────────────────────────────────   │
  ├──────────────────────────────────────────────────────┤
  │  [▶ Start Tool]  [■ Stop]  [↺ Reset Renderer] [✕]   │
  └──────────────────────────────────────────────────────┘

Author  : Loc Vu Trung / LVT Center
License : GPL-3.0
"""

import contextlib

from qgis.PyQt.QtCore import Qt, QSize
from ..shared.qt_compat import QVariant
from qgis.PyQt.QtGui import QColor, QIcon, QPixmap, QPainter
from qgis.PyQt.QtWidgets import (
    QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
    QGroupBox, QLabel, QPushButton, QCheckBox, QRadioButton,
    QButtonGroup, QTableWidget, QTableWidgetItem, QHeaderView,
    QColorDialog, QAbstractItemView, QMessageBox, QLineEdit,
    QSizePolicy, QWidget, QToolButton, QComboBox,
    QSpinBox, QSlider,
)
from qgis.core import (
    QgsMapLayerProxyModel, QgsFieldProxyModel,
    QgsVectorLayer, QgsField, QgsEditorWidgetSetup,
    QgsRuleBasedRenderer, QgsSymbol,
    QgsWkbTypes, QgsProject,
    QgsSimpleFillSymbolLayer, QgsFillSymbol,
)
from qgis.gui import QgsMapLayerComboBox

from ..shared.qt_compat import QtCompat
from ..shared.i18n import tr

# Numeric QVariant types for count field filtering
_NUMERIC_TYPES = {
    QVariant.Int, QVariant.LongLong, QVariant.Double,
    QVariant.UInt, QVariant.ULongLong,
}

# QgsField type constants — works across all QGIS/Qt versions
try:
    from ..shared.qt_compat import QVariant as _QV
    _FIELD_INT    = _QV.Int
    _FIELD_STRING = _QV.String
except ImportError:
    _FIELD_INT    = 2   # QVariant.Int == 2
    _FIELD_STRING = 10  # QVariant.String == 10

from .map_tool import ClickClassifyTool


# ─── Default classification steps ────────────────────────────────────
_DEFAULT_STEPS = [
    {'value': 0, 'label': '',      'color': '#aaaaaa'},
    {'value': 1, 'label': 'Good',  'color': '#2ecc71'},
    {'value': 2, 'label': 'Fair',  'color': '#f39c12'},
    {'value': 3, 'label': 'Poor',  'color': '#e74c3c'},
]

# ─── Default polygon overlay style ───────────────────────────────────
_DEFAULT_POLY_STYLE = {
    'outline_only': True,       # True = no fill; best for satellite basemap
    'border_color': '#00FF00',  # bright green — high contrast on sat imagery
    'border_width': 2,
    'fill_color':   '#FFFF00',  # yellow fill (used when outline_only=False)
    'fill_opacity': 30,         # 0–100%
}


def _color_icon(hex_color: str, size: int = 20) -> QIcon:
    """Create a solid-color square QIcon for color picker buttons."""
    pix = QPixmap(size, size)
    pix.fill(QColor(hex_color))
    painter = QPainter(pix)
    painter.setPen(QColor('#555555'))
    painter.drawRect(0, 0, size - 1, size - 1)
    painter.end()
    return QIcon(pix)


class ClickClassifyDialog(QDialog):
    """Non-modal dialog for configuring and running the Click-Classify tool."""

    def __init__(self, iface, parent=None):
        super().__init__(parent or iface.mainWindow())
        self.iface = iface
        self.canvas = iface.mapCanvas()
        self._map_tool = None
        self._prev_tool = None
        self._orig_renderer = {}   # layer_id → original renderer (for restore)
        self._steps = [dict(s) for s in _DEFAULT_STEPS]
        self._compact = False      # True when tool is active (mini-mode)
        # Polygon overlay style — persists between tool opens
        self._poly_style = dict(_DEFAULT_POLY_STYLE)

        self.setWindowTitle(tr('🖱️ Click-Classify Tool'))
        self.setMinimumWidth(520)
        self.setWindowFlags(
            self.windowFlags() | Qt.WindowType.WindowStaysOnTopHint
        )

        self._build_ui()
        self._connect_signals()

    # ------------------------------------------------------------------
    # UI construction
    # ------------------------------------------------------------------

    def _build_ui(self):
        root = QVBoxLayout(self)
        root.setSpacing(8)
        root.setContentsMargins(10, 10, 10, 10)

        # ── Layer / Field config ──────────────────────────────────────
        grp_layer = QGroupBox(tr('Layer & Fields'))
        fl = QFormLayout(grp_layer)

        self.cmb_layer = QgsMapLayerComboBox()
        self.cmb_layer.setFilters(
            QgsMapLayerProxyModel.Filter.VectorLayer
        )
        fl.addRow(tr('Layer:'), self.cmb_layer)

        # Count field row — editable: choose existing OR type a new name
        frow = QHBoxLayout()
        self.cmb_field = QComboBox()
        self.cmb_field.setEditable(True)
        self.cmb_field.setInsertPolicy(QComboBox.InsertPolicy.NoInsert)
        self.cmb_field.lineEdit().setPlaceholderText(
            tr('Select or type field name…')
        )
        self.cmb_field.setToolTip(
            tr('Pick an existing numeric field OR type a new name,\nthen click "+ New field" to create it.')
        )
        frow.addWidget(self.cmb_field)
        self.btn_new_field = QPushButton(tr('+ New field'))
        self.btn_new_field.setFixedWidth(110)
        self.btn_new_field.setToolTip(
            tr('Create a new Integer field with the name typed above')
        )
        frow.addWidget(self.btn_new_field)
        fl.addRow(tr('Count field:'), frow)

        # Label field row — editable: choose existing OR type a new name
        lrow = QHBoxLayout()
        self.chk_label_field = QCheckBox(tr('Write label text:'))
        self.chk_label_field.setChecked(False)
        self.cmb_label_field = QComboBox()
        self.cmb_label_field.setEditable(True)
        self.cmb_label_field.setInsertPolicy(QComboBox.InsertPolicy.NoInsert)
        self.cmb_label_field.lineEdit().setPlaceholderText(
            tr('Select or type field name…')
        )
        self.cmb_label_field.setToolTip(
            tr('Pick an existing text field OR type a new name,\nthen click "+ New" to create it.')
        )
        self.cmb_label_field.setEnabled(False)
        self.btn_new_label_field = QPushButton(tr('+ New'))
        self.btn_new_label_field.setFixedWidth(70)
        self.btn_new_label_field.setEnabled(False)
        self.btn_new_label_field.setToolTip(
            tr('Create a new Text field with the name typed above')
        )
        lrow.addWidget(self.chk_label_field)
        lrow.addWidget(self.cmb_label_field, 1)
        lrow.addWidget(self.btn_new_label_field)
        fl.addRow('', lrow)

        root.addWidget(grp_layer)

        # ── Click schedule table ──────────────────────────────────────
        grp_steps = QGroupBox(tr('Click schedule'))
        sv = QVBoxLayout(grp_steps)

        # Table
        self.tbl = QTableWidget(0, 4)
        self.tbl.setHorizontalHeaderLabels([
            tr('Step'), tr('Value'), tr('Label'), tr('Colour')
        ])
        self.tbl.horizontalHeader().setSectionResizeMode(
            0, QHeaderView.ResizeMode.ResizeToContents
        )
        self.tbl.horizontalHeader().setSectionResizeMode(
            1, QHeaderView.ResizeMode.ResizeToContents
        )
        self.tbl.horizontalHeader().setSectionResizeMode(
            2, QHeaderView.ResizeMode.Stretch
        )
        self.tbl.horizontalHeader().setSectionResizeMode(
            3, QHeaderView.ResizeMode.Fixed
        )
        self.tbl.setColumnWidth(3, 110)
        self.tbl.setSelectionBehavior(
            QAbstractItemView.SelectionBehavior.SelectRows
        )
        self.tbl.setEditTriggers(
            QAbstractItemView.EditTrigger.DoubleClicked |
            QAbstractItemView.EditTrigger.SelectedClicked |
            QAbstractItemView.EditTrigger.EditKeyPressed
        )
        self.tbl.setAlternatingRowColors(True)
        sv.addWidget(self.tbl)

        # Hint for Label column
        lbl_hint = QLabel(
            tr('✏ Click a row to select, then click the Label cell to edit freely '
               '(e.g. Tốt / Xấu / TB / 1 / 2 / 3…)')
        )
        lbl_hint.setStyleSheet(
            'color:#777; font-size:10px; font-style:italic; padding:1px 0;'
        )
        lbl_hint.setWordWrap(True)
        sv.addWidget(lbl_hint)

        # Add / Remove buttons
        btn_row = QHBoxLayout()
        self.btn_add = QPushButton(tr('+ Add step'))
        self.btn_remove = QPushButton(tr('− Remove'))
        self.btn_remove.setEnabled(False)
        btn_row.addStretch()
        btn_row.addWidget(self.btn_add)
        btn_row.addWidget(self.btn_remove)
        sv.addLayout(btn_row)

        root.addWidget(grp_steps)

        # ── Polygon overlay style ─────────────────────────────────────
        root.addWidget(self._build_poly_style_group())

        # ── Save options ──────────────────────────────────────────────
        grp_save = QGroupBox(tr('Save options'))
        sh = QHBoxLayout(grp_save)
        self.rb_auto = QRadioButton(tr('Auto-save each click'))
        self.rb_manual = QRadioButton(tr('Manual save'))
        self.rb_auto.setChecked(True)
        self.btn_save_now = QPushButton(tr('💾 Save now'))
        self.btn_save_now.setEnabled(False)
        sh.addWidget(self.rb_auto)
        sh.addWidget(self.rb_manual)
        sh.addStretch()
        sh.addWidget(self.btn_save_now)
        root.addWidget(grp_save)

        # ── Status bar ────────────────────────────────────────────────
        self.lbl_status = QLabel(tr('Ready. Configure above then press Start.'))
        self.lbl_status.setStyleSheet(
            'color: #555; background: #f5f5f5; border-radius: 3px; padding: 3px 6px;'
        )
        self.lbl_status.setWordWrap(True)
        root.addWidget(self.lbl_status)

        # ── Compact status (shown only in mini-mode) ──────────────────
        self.wgt_compact = QWidget()
        ch = QHBoxLayout(self.wgt_compact)
        ch.setContentsMargins(4, 4, 4, 4)
        ch.setSpacing(6)
        self.lbl_compact_step = QLabel('')
        self.lbl_compact_step.setStyleSheet(
            'font-weight:bold; font-size:11px; color:#27ae60;'
        )
        self.lbl_compact_status = QLabel('')
        self.lbl_compact_status.setStyleSheet('font-size:11px; color:#555;')
        self.lbl_compact_status.setSizePolicy(
            QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred
        )
        ch.addWidget(self.lbl_compact_step)
        ch.addWidget(self.lbl_compact_status, 1)
        self.wgt_compact.hide()
        root.addWidget(self.wgt_compact)

        # ── Action buttons ────────────────────────────────────────────
        btn_bar = QHBoxLayout()
        self.btn_start = QPushButton(tr('▶ Start'))
        self.btn_start.setStyleSheet(
            'background:#27ae60;color:white;font-weight:bold;padding:5px 14px;'
        )
        self.btn_stop = QPushButton(tr('■ Stop'))
        self.btn_stop.setEnabled(False)
        self.btn_stop.setStyleSheet(
            'background:#c0392b;color:white;font-weight:bold;padding:5px 14px;'
        )
        self.btn_expand = QPushButton(tr('⛶ Config'))
        self.btn_expand.setToolTip(tr('Show/hide configuration panel'))
        self.btn_expand.setCheckable(True)
        self.btn_expand.setChecked(True)
        self.btn_expand.setStyleSheet('padding:5px 10px;')
        self.btn_reset_renderer = QPushButton(tr('↺ Restore'))
        self.btn_reset_renderer.setToolTip(
            tr('Restore the original renderer before Click-Classify was activated')
        )
        self.btn_close = QPushButton('✕')
        self.btn_close.setFixedWidth(32)

        btn_bar.addWidget(self.btn_start)
        btn_bar.addWidget(self.btn_stop)
        btn_bar.addWidget(self.btn_expand)
        btn_bar.addWidget(self.btn_reset_renderer)
        btn_bar.addStretch()
        btn_bar.addWidget(self.btn_close)
        root.addLayout(btn_bar)

        # Populate table with defaults
        self._populate_table()

    def _connect_signals(self):
        self.cmb_layer.layerChanged.connect(self._on_layer_changed)
        self.chk_label_field.toggled.connect(self._on_label_field_toggled)
        self.btn_new_field.clicked.connect(self._create_count_field)
        self.btn_new_label_field.clicked.connect(self._create_label_field)
        self.btn_add.clicked.connect(self._add_step)
        self.btn_remove.clicked.connect(self._remove_step)
        self.tbl.itemSelectionChanged.connect(self._on_selection_changed)
        self.rb_manual.toggled.connect(self._on_manual_toggled)
        self.btn_save_now.clicked.connect(self._save_now)
        self.btn_start.clicked.connect(self._start_tool)
        self.btn_stop.clicked.connect(self._stop_tool)
        self.btn_expand.toggled.connect(self._on_expand_toggled)
        self.btn_reset_renderer.clicked.connect(self._restore_renderer)
        self.btn_close.clicked.connect(self.close)

        self._on_layer_changed(self.cmb_layer.currentLayer())

    # ------------------------------------------------------------------
    # Polygon style panel
    # ------------------------------------------------------------------

    def _build_poly_style_group(self) -> QGroupBox:
        """Build the Polygon Overlay Style group box.

        Controls let the user choose how polygons are drawn on the map
        canvas while Click-Classify is active.  Settings persist between
        tool opens via ``self._poly_style``.

        Returns:
            Fully-constructed QGroupBox with all controls connected.
        """
        ps = self._poly_style
        grp = QGroupBox(tr('Polygon Overlay Style  (for satellite basemap)'))
        gl = QVBoxLayout(grp)
        gl.setSpacing(5)

        # ── Row 1: outline-only toggle + Reset ──────────────────────
        row1 = QHBoxLayout()
        self.chk_outline_only = QCheckBox(
            tr('🗎 Outline only — no fill (see through to basemap)'))
        self.chk_outline_only.setChecked(ps.get('outline_only', True))
        self.chk_outline_only.setToolTip(
            tr('When checked, polygons show only a coloured border.\n'
               'Recommended when classifying on satellite imagery.'))
        row1.addWidget(self.chk_outline_only)
        row1.addStretch()
        self.btn_poly_reset = QPushButton(tr('↻ Reset'))
        self.btn_poly_reset.setFixedWidth(70)
        self.btn_poly_reset.setToolTip(tr('Reset polygon style to factory defaults'))
        self.btn_poly_reset.clicked.connect(self._reset_poly_style)
        row1.addWidget(self.btn_poly_reset)
        gl.addLayout(row1)

        # ── Row 2: border color + width ─────────────────────────────
        row2 = QHBoxLayout()
        row2.addWidget(QLabel(tr('Border color:')))
        bc = ps.get('border_color', '#00FF00')
        self.btn_ps_border = QPushButton(bc)
        self.btn_ps_border.setFixedWidth(90)
        self.btn_ps_border.setStyleSheet(
            f'background-color:{bc}; color:#000')
        self.btn_ps_border.clicked.connect(
            lambda: self._pick_ps_color('border_color', self.btn_ps_border))
        row2.addWidget(self.btn_ps_border)
        row2.addSpacing(14)
        row2.addWidget(QLabel(tr('Width (px):')))
        self.spn_ps_bw = QSpinBox()
        self.spn_ps_bw.setRange(1, 8)
        self.spn_ps_bw.setValue(ps.get('border_width', 2))
        self.spn_ps_bw.setFixedWidth(54)
        self.spn_ps_bw.valueChanged.connect(
            lambda v: self._poly_style.update({'border_width': v}))
        row2.addWidget(self.spn_ps_bw)
        row2.addStretch()
        gl.addLayout(row2)

        # ── Row 3: fill color + opacity (hidden when outline_only) ───
        self._wgt_fill_row = QWidget()
        row3 = QHBoxLayout(self._wgt_fill_row)
        row3.setContentsMargins(0, 0, 0, 0)
        row3.addWidget(QLabel(tr('Fill color:')))
        fc = ps.get('fill_color', '#FFFF00')
        self.btn_ps_fill = QPushButton(fc)
        self.btn_ps_fill.setFixedWidth(90)
        self.btn_ps_fill.setStyleSheet(
            f'background-color:{fc}; color:#000')
        self.btn_ps_fill.clicked.connect(
            lambda: self._pick_ps_color('fill_color', self.btn_ps_fill))
        row3.addWidget(self.btn_ps_fill)
        row3.addSpacing(14)
        row3.addWidget(QLabel(tr('Opacity:')))
        self.sld_ps_op = QSlider(Qt.Orientation.Horizontal)
        self.sld_ps_op.setRange(0, 100)
        self.sld_ps_op.setValue(ps.get('fill_opacity', 30))
        self.sld_ps_op.setFixedWidth(90)
        self.sld_ps_op.valueChanged.connect(
            lambda v: self._poly_style.update({'fill_opacity': v}))
        row3.addWidget(self.sld_ps_op)
        self.lbl_ps_op = QLabel(f'{ps.get("fill_opacity", 30)}%')
        self.lbl_ps_op.setFixedWidth(34)
        self.sld_ps_op.valueChanged.connect(
            lambda v: self.lbl_ps_op.setText(f'{v}%'))
        row3.addWidget(self.lbl_ps_op)
        row3.addStretch()
        gl.addWidget(self._wgt_fill_row)

        # ── Hint ────────────────────────────────────────────────────
        hint = QLabel(
            tr('💡 Step colors (table above) become border colors — '
               'each class gets a distinct outline for easy identification.'))
        hint.setStyleSheet(
            'color:#777;font-size:10px;font-style:italic;padding:2px 0;')
        hint.setWordWrap(True)
        gl.addWidget(hint)

        # ── Wire outline-only toggle ────────────────────────────────
        self._wgt_fill_row.setVisible(not ps.get('outline_only', True))
        self.chk_outline_only.toggled.connect(self._on_outline_only_toggled)

        return grp

    def _on_outline_only_toggled(self, checked: bool):
        """Show/hide fill controls and persist the outline_only flag."""
        self._poly_style['outline_only'] = checked
        self._wgt_fill_row.setVisible(not checked)
        self.adjustSize()

    def _pick_ps_color(self, key: str, btn: QPushButton):
        """Open color dialog for a polygon style color button."""
        current = QColor(btn.text())
        color = QColorDialog.getColor(current, self, tr('Choose colour'))
        if color.isValid():
            hex_c = color.name()
            btn.setText(hex_c)
            btn.setStyleSheet(f'background-color:{hex_c}; color:#000')
            self._poly_style[key] = hex_c

    def _reset_poly_style(self):
        """Reset polygon overlay style to factory defaults."""
        self._poly_style = dict(_DEFAULT_POLY_STYLE)
        ps = self._poly_style
        self.chk_outline_only.setChecked(ps['outline_only'])
        self.btn_ps_border.setText(ps['border_color'])
        self.btn_ps_border.setStyleSheet(
            f'background-color:{ps["border_color"]}; color:#000')
        self.spn_ps_bw.setValue(ps['border_width'])
        self.btn_ps_fill.setText(ps['fill_color'])
        self.btn_ps_fill.setStyleSheet(
            f'background-color:{ps["fill_color"]}; color:#000')
        self.sld_ps_op.setValue(ps['fill_opacity'])
        self._set_status(tr('Polygon style reset to default.'))

    # ------------------------------------------------------------------
    # Table helpers
    # ------------------------------------------------------------------

    def _populate_table(self):
        """Rebuild the table from self._steps."""
        self.tbl.setRowCount(0)
        for i, step in enumerate(self._steps):
            self._insert_row(i, step)

    def _insert_row(self, row: int, step: dict):
        self.tbl.insertRow(row)

        # Step index (read-only)
        item_step = QTableWidgetItem(str(row))
        item_step.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
        item_step.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
        self.tbl.setItem(row, 0, item_step)

        # Value (editable integer)
        item_val = QTableWidgetItem(str(step['value']))
        item_val.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
        self.tbl.setItem(row, 1, item_val)

        # Label (editable text)
        item_lbl = QTableWidgetItem(step['label'])
        self.tbl.setItem(row, 2, item_lbl)

        # Colour (button)
        btn_col = QPushButton()
        btn_col.setIcon(_color_icon(step['color']))
        btn_col.setText(step['color'])
        btn_col.setIconSize(QSize(20, 20))
        btn_col.clicked.connect(lambda checked, r=row: self._pick_color(r))
        self.tbl.setCellWidget(row, 3, btn_col)

    def _sync_steps_from_table(self) -> bool:
        """Read table contents back into self._steps. Returns False on error."""
        steps = []
        seen_values = set()
        for row in range(self.tbl.rowCount()):
            val_item = self.tbl.item(row, 1)
            lbl_item = self.tbl.item(row, 2)
            btn = self.tbl.cellWidget(row, 3)

            try:
                val = int(val_item.text()) if val_item else row
            except ValueError:
                self._set_status(
                    tr(f'Row {row}: value must be an integer.'), error=True
                )
                return False

            if val in seen_values:
                self._set_status(
                    tr(f'Duplicate value {val} in row {row}.'), error=True
                )
                return False
            seen_values.add(val)

            color = btn.text() if btn else '#aaaaaa'
            label = lbl_item.text() if lbl_item else ''
            steps.append({'value': val, 'label': label, 'color': color})

        self._steps = steps
        return True

    def _add_step(self):
        """Add a new step row."""
        next_val = max((s['value'] for s in self._steps), default=-1) + 1
        new_step = {'value': next_val, 'label': f'Step {next_val}', 'color': '#3498db'}
        self._steps.append(new_step)
        row = self.tbl.rowCount()
        self._insert_row(row, new_step)
        self._refresh_step_numbers()

    def _remove_step(self):
        """Remove the selected row."""
        rows = sorted(
            {i.row() for i in self.tbl.selectedItems()}, reverse=True
        )
        for r in rows:
            self.tbl.removeRow(r)
        self._refresh_step_numbers()

    def _refresh_step_numbers(self):
        for row in range(self.tbl.rowCount()):
            item = self.tbl.item(row, 0)
            if item:
                item.setText(str(row))

    def _pick_color(self, row: int):
        """Open a colour picker dialog and update the row colour button."""
        btn = self.tbl.cellWidget(row, 3)
        current = QColor(btn.text() if btn else '#aaaaaa')
        color = QColorDialog.getColor(current, self, tr('Choose colour'))
        if color.isValid():
            hex_c = color.name()
            btn.setIcon(_color_icon(hex_c))
            btn.setText(hex_c)

    # ------------------------------------------------------------------
    # Event handlers
    # ------------------------------------------------------------------

    def _on_layer_changed(self, layer):
        """Repopulate the editable field combos with fields from the new layer."""
        self._populate_field_combo(
            self.cmb_field, layer,
            numeric=True,
            default_name='click_count',
        )
        self._populate_field_combo(
            self.cmb_label_field, layer,
            numeric=False,
            default_name='click_label',
        )

    def _populate_field_combo(
        self,
        combo: QComboBox,
        layer,
        numeric: bool = True,
        default_name: str = '',
    ):
        """Fill an editable QComboBox with compatible fields from layer.

        Keeps any text the user already typed.
        """
        prev_text = combo.currentText()
        combo.blockSignals(True)
        combo.clear()
        if layer and isinstance(layer, QgsVectorLayer):
            for fld in layer.fields():
                if numeric:
                    if fld.type() in _NUMERIC_TYPES:
                        combo.addItem(fld.name())
                else:
                    if fld.type() == QVariant.String:
                        combo.addItem(fld.name())
        combo.blockSignals(False)
        # Restore previous selection / text
        if prev_text:
            idx = combo.findText(prev_text)
            if idx >= 0:
                combo.setCurrentIndex(idx)
            else:
                combo.setCurrentText(prev_text)
        elif default_name:
            idx = combo.findText(default_name)
            if idx >= 0:
                combo.setCurrentIndex(idx)

    def _on_label_field_toggled(self, checked: bool):
        self.cmb_label_field.setEnabled(checked)
        self.btn_new_label_field.setEnabled(checked)

    def _on_selection_changed(self):
        self.btn_remove.setEnabled(bool(self.tbl.selectedItems()))

    def _on_manual_toggled(self, checked: bool):
        self.btn_save_now.setEnabled(checked and self._map_tool is not None)

    # ------------------------------------------------------------------
    # Field creation
    # ------------------------------------------------------------------

    def _create_count_field(self):
        """Create an Integer field using the name typed/selected by the user."""
        layer = self.cmb_layer.currentLayer()
        name = self.cmb_field.currentText().strip() or 'click_count'
        self._add_field_to_layer(layer, name, _FIELD_INT)
        self._on_layer_changed(layer)
        self.cmb_field.setCurrentText(name)

    def _create_label_field(self):
        """Create a String field using the name typed/selected by the user."""
        layer = self.cmb_layer.currentLayer()
        name = self.cmb_label_field.currentText().strip() or 'click_label'
        self._add_field_to_layer(layer, name, _FIELD_STRING, 80)
        self._on_layer_changed(layer)
        self.cmb_label_field.setCurrentText(name)

    def _add_field_to_layer(self, layer, name: str,
                             ftype=None, length: int = 0):
        """Add a field to a vector layer, compatible with all QGIS 3.x versions.

        Strategy:
        1. Try QgsField with provided ftype (works on most QGIS versions).
        2. Fall back to raw int literal (legacy Qt5 path).
        3. Verify the field was actually written after commitChanges().
        4. Show a clear error dialog on any failure.

        Args:
            layer:  Target QgsVectorLayer.
            name:   New field name (Unicode-safe).
            ftype:  Type constant — use _FIELD_INT or _FIELD_STRING.
                    Defaults to _FIELD_INT.
            length: Field length (for String fields, e.g. 80).
        """
        import warnings
        if ftype is None:
            ftype = _FIELD_INT
        if not layer or not isinstance(layer, QgsVectorLayer):
            QMessageBox.warning(
                self, tr('Error'), tr('No vector layer selected.'))
            return
        name = (name or '').strip()
        if not name:
            QMessageBox.warning(
                self, tr('Error'), tr('Field name cannot be empty.'))
            return
        if layer.fields().indexFromName(name) >= 0:
            self._set_status(tr(f'Field "{name}" already exists.'))
            return

        # QString == 10 in both Qt5 QVariant and Qt6 QMetaType.
        try:
            int_ftype = int(ftype)
        except (TypeError, ValueError):
            int_ftype = 2
        is_string = (int_ftype == 10)
        type_name = 'String' if is_string else 'Integer'
        raw_fallback = 10 if is_string else 2

        # ── Build QgsField (try ftype, fallback to raw int) ────────
        field_obj = None
        for candidate in (ftype, raw_fallback):
            try:
                with warnings.catch_warnings():
                    warnings.simplefilter('ignore')
                    field_obj = QgsField(name, candidate, type_name, length)
                break
            except Exception:
                field_obj = None

        if field_obj is None:
            QMessageBox.warning(
                self, tr('Error'),
                tr(f'Cannot build QgsField "{name}" (type={type_name}).\n'
                   f'Requires QGIS 3.16 or later.'))
            return

        # ── Apply to layer ─────────────────────────────────────────
        try:
            was_editing = layer.isEditable()
            if not was_editing:
                layer.startEditing()

            ok = layer.addAttribute(field_obj)
            if not ok:
                if not was_editing:
                    layer.rollBack()
                QMessageBox.warning(
                    self, tr('Error'),
                    tr(f'Cannot add field "{name}" to this layer.\n'
                       f'The layer format may be read-only or memory-only.\n'
                       f'Supported formats: Shapefile, GeoPackage, PostGIS.'))
                return

            layer.commitChanges()

            # Sanity-check: some formats silently skip the field
            if layer.fields().indexFromName(name) < 0:
                QMessageBox.warning(
                    self, tr('Error'),
                    tr(f'Field "{name}" was not saved after commit.\n'
                       f'Check that the file is not locked by another app.'))
                return

            self._set_status(
                tr(f'✅ Field "{name}" ({type_name}) created successfully.'))

        except Exception as exc:
            with contextlib.suppress(Exception):
                layer.rollBack()
            QMessageBox.warning(
                self, tr('Error'),
                tr(f'Unexpected error creating field "{name}":\n{exc}'))

    # ------------------------------------------------------------------
    # Tool activation
    # ------------------------------------------------------------------

    def _build_config(self) -> dict | None:
        """Validate inputs and build the config dict for ClickClassifyTool."""
        layer = self.cmb_layer.currentLayer()
        if not layer or not isinstance(layer, QgsVectorLayer):
            QMessageBox.warning(self, tr('Error'), tr('Please select a vector layer.'))
            return None

        field = self.cmb_field.currentText().strip()
        if not field:
            QMessageBox.warning(self, tr('Error'), tr('Please select or enter a count field name.'))
            return None

        # Auto-create field if it doesn't exist yet
        if layer.fields().indexFromName(field) < 0:
            self._add_field_to_layer(layer, field, _FIELD_INT)
            self._on_layer_changed(layer)
            self.cmb_field.setCurrentText(field)

        if not self._sync_steps_from_table():
            return None

        if len(self._steps) < 2:
            QMessageBox.warning(
                self, tr('Error'),
                tr('Define at least 2 steps (step 0 = unclassified).')
            )
            return None

        label_field = ''
        if self.chk_label_field.isChecked():
            label_field = self.cmb_label_field.currentText().strip()
            # Auto-create label field if typed but doesn't exist
            if label_field and layer.fields().indexFromName(label_field) < 0:
                self._add_field_to_layer(layer, label_field, _FIELD_STRING, 80)
                self._on_layer_changed(layer)
                self.cmb_label_field.setCurrentText(label_field)


        return {
            'layer':       layer,
            'field':       field,
            'label_field': label_field,
            'steps':       self._steps,
            'auto_save':   self.rb_auto.isChecked(),
        }

    def _start_tool(self):
        """Activate the map tool and switch to compact mode."""
        config = self._build_config()
        if config is None:
            return

        layer = config['layer']

        # Backup original renderer
        if layer.id() not in self._orig_renderer:
            orig = layer.renderer()
            if orig:
                self._orig_renderer[layer.id()] = orig.clone()

        # Apply rule-based renderer
        self._apply_renderer(layer, config['field'], config['steps'])

        # Ensure layer is editable
        if not layer.isEditable():
            layer.startEditing()

        # Create and activate map tool
        self._prev_tool = self.canvas.mapTool()
        self._map_tool = ClickClassifyTool(self.canvas, config)
        self._map_tool.classified.connect(self._on_classified)
        self._map_tool.status_changed.connect(self._set_status)
        self.canvas.setMapTool(self._map_tool)

        self.btn_start.setEnabled(False)
        self.btn_stop.setEnabled(True)
        if self.rb_manual.isChecked():
            self.btn_save_now.setEnabled(True)
        self._set_status(tr('✅ Tool active. Click on features to classify.'))

        # Switch to compact mode automatically
        self._set_compact(True)

    def _stop_tool(self):
        """Deactivate the map tool, restore previous tool, expand panel."""
        if self._map_tool:
            self.canvas.unsetMapTool(self._map_tool)
            self._map_tool = None

        if self._prev_tool:
            self.canvas.setMapTool(self._prev_tool)
            self._prev_tool = None

        # Commit any pending edits
        layer = self.cmb_layer.currentLayer()
        if layer and layer.isEditable():
            layer.commitChanges()

        self.btn_start.setEnabled(True)
        self.btn_stop.setEnabled(False)
        self.btn_save_now.setEnabled(False)
        self._set_status(tr('Tool stopped. Edits saved.'))

        # Return to full config mode
        self._set_compact(False)

    # ------------------------------------------------------------------
    # Renderer
    # ------------------------------------------------------------------

    def _make_symbol(self, geom_type, step_color: str):
        """Build a single QgsSymbol for one classification step.

        For polygon layers, applies the user-configured overlay style
        (outline-only vs filled) with the step's color used for the
        outline/border.  Fill color and opacity come from _poly_style.

        Args:
            geom_type: QgsWkbTypes.GeometryType value.
            step_color: hex color string for this step.

        Returns:
            QgsSymbol ready to attach to a rule.
        """
        if geom_type == QgsWkbTypes.GeometryType.PolygonGeometry:
            # Build fill symbol using QgsSimpleFillSymbolLayer for
            # fine-grained control over fill/border separately.
            ps = self._poly_style
            border_color = QColor(ps.get('border_color', '#00FF00'))
            border_width  = float(ps.get('border_width', 2))
            outline_only  = ps.get('outline_only', True)

            if outline_only:
                fill_color = QColor(0, 0, 0, 0)   # fully transparent
            else:
                fc = QColor(ps.get('fill_color', '#FFFF00'))
                opacity = max(0, min(100, int(ps.get('fill_opacity', 30))))
                fc.setAlpha(int(opacity * 2.55))
                fill_color = fc

            # Step color overrides border for classification feedback
            border_color = QColor(step_color)

            sl = QgsSimpleFillSymbolLayer()
            sl.setFillColor(fill_color)
            sl.setStrokeColor(border_color)
            sl.setStrokeWidth(border_width)

            sym = QgsFillSymbol()
            sym.changeSymbolLayer(0, sl)
            return sym

        # Point / Line: use default symbol with step color
        sym = QgsSymbol.defaultSymbol(geom_type)
        if sym:
            sym.setColor(QColor(step_color))
        return sym

    def _apply_renderer(self, layer: QgsVectorLayer,
                        field: str, steps: list):
        """Build and apply a Rule-based renderer from the step config."""
        root = QgsRuleBasedRenderer.Rule(None)
        geom_type = layer.geometryType()

        for step in steps:
            sym = self._make_symbol(geom_type, step['color'])
            label = step['label'] or str(step['value'])
            rule = QgsRuleBasedRenderer.Rule(sym)
            rule.setFilterExpression(f'"{field}" = {step["value"]}')
            rule.setLabel(label)
            root.appendChild(rule)

        # Catch-all for unset/unclassified features
        sym_else = self._make_symbol(geom_type, '#cccccc')
        rule_else = QgsRuleBasedRenderer.Rule(sym_else)
        rule_else.setFilterExpression('')
        rule_else.setLabel(tr('(unclassified)'))
        rule_else.setIsElse(True)
        root.appendChild(rule_else)

        renderer = QgsRuleBasedRenderer(root)
        layer.setRenderer(renderer)
        layer.triggerRepaint()
        self._set_status(tr('Renderer updated.'))

    def _restore_renderer(self):
        """Restore the layer to its original renderer."""
        layer = self.cmb_layer.currentLayer()
        if not layer:
            return
        orig = self._orig_renderer.pop(layer.id(), None)
        if orig:
            layer.setRenderer(orig)
            layer.triggerRepaint()
            self._set_status(tr('Original renderer restored.'))
        else:
            self._set_status(tr('No backup renderer found for this layer.'))

    # ------------------------------------------------------------------
    # Save helpers
    # ------------------------------------------------------------------

    def _save_now(self):
        """Manually commit pending edits."""
        layer = self.cmb_layer.currentLayer()
        if layer and layer.isEditable():
            layer.commitChanges()
            layer.startEditing()
            self._set_status(tr('Changes saved.'))

    # ------------------------------------------------------------------
    # Misc
    # ------------------------------------------------------------------

    def _on_classified(self, layer_id, fid, value, label):
        display = label or str(value)
        msg = f'✔ Feature {fid} → {display}'
        self._set_status(msg)
        # Update compact indicator
        self.lbl_compact_status.setText(msg)
        if self._map_tool:
            steps = self._map_tool.config.get('steps', [])
            total = len(steps)
            step_labels = ' | '.join(
                f'<span style="color:{s["color"]}">●</span> {s["label"] or s["value"]}'
                for s in steps if s.get('label') or s.get('value') != 0
            )
            self.lbl_compact_step.setText(f'{total} steps')

    def _set_status(self, msg: str, error: bool = False):
        color = '#c0392b' if error else '#27ae60' if msg.startswith('✔') else '#555'
        self.lbl_status.setStyleSheet(
            f'color:{color};background:#f5f5f5;'
            f'border-radius:3px;padding:3px 6px;'
        )
        self.lbl_status.setText(msg)
        self.lbl_compact_status.setText(msg)

    # ------------------------------------------------------------------
    # Compact / expand mode
    # ------------------------------------------------------------------

    def _set_compact(self, compact: bool):
        """Switch between full config mode and compact active-tool mode."""
        self._compact = compact
        # Config groups
        self.grp_layer = self.findChild(QGroupBox, '')
        for child in self.findChildren(QGroupBox):
            child.setVisible(not compact)
        self.grp_save = None  # reset
        # Status bars
        self.lbl_status.setVisible(not compact)
        self.wgt_compact.setVisible(compact)
        # Expand button state
        self.btn_expand.setChecked(not compact)
        self.btn_expand.setVisible(compact)
        # Resize window
        if compact:
            layer = self.cmb_layer.currentLayer()
            layer_name = layer.name() if layer else '?'
            # cmb_field is a plain QComboBox (not QgsFieldComboBox),
            # so use currentText() not currentField().
            field = self.cmb_field.currentText()
            step_info = '  '.join(
                f'<font color="{s["color"]}">●</font> {s["label"] or s["value"]}'
                for s in self._steps
            )
            self.lbl_compact_step.setText(
                f'🖱️ <b>{layer_name}</b> [{field}]'
            )
            self.lbl_compact_status.setText(tr('Click on a feature...'))
            self.setWindowTitle(tr('🖱️ Click-Classify [ACTIVE]'))
            self.adjustSize()
        else:
            self.setWindowTitle(tr('🖱️ Click-Classify Tool'))
            self.adjustSize()

    def _on_expand_toggled(self, checked: bool):
        """Toggle config panel visibility manually during active mode."""
        for child in self.findChildren(QGroupBox):
            child.setVisible(checked)
        self.lbl_status.setVisible(checked)
        self.wgt_compact.setVisible(not checked)
        self.btn_expand.setText(tr('⛶ Config') if not checked else tr('▲ Hide'))
        self.adjustSize()

    def refresh_layers(self):
        """Refresh layer combo (called by plugin on re-open)."""
        self.cmb_layer.setLayer(self.cmb_layer.currentLayer())

    def closeEvent(self, event):
        """Stop the tool when the dialog is closed."""
        self._stop_tool()
        super().closeEvent(event)
