"""Tab 3: Merge multiple SHP files into a single KML / KMZ.

Provides full label / popup / style / highlight configuration per layer.
Each geometry tab has a dropdown to select which layer to configure.
"""

import os
import json
from qgis.PyQt.QtWidgets import (
    QWidget, QVBoxLayout, QHBoxLayout, QTabWidget,
    QLabel, QComboBox, QLineEdit, QPushButton, QSpinBox,
    QFileDialog, QMessageBox, QTableWidget, QTableWidgetItem,
    QHeaderView, QCheckBox, QColorDialog, QGroupBox,
    QSlider, QScrollArea, QFrame, QDoubleSpinBox, QSplitter,
    QTextEdit
)
from qgis.PyQt.QtCore import Qt, QPoint
from qgis.PyQt.QtGui import (
    QColor, QPainter, QPen, QBrush, QPolygon, QPainterPath
)
from qgis.core import QgsWkbTypes, QgsProject, QgsVectorLayer
from .merge_builder import MergeKmlBuilder
from .html_template import HtmlTemplateBuilder
from ..shared.qt_compat import QtCompat, MsgButton


# ── Helpers for label format migration ───────────────

def _old_name_cfg_to_rows(name_cfg):
    """Convert old field1/field2/field3 config dict → new rows list."""
    f1 = name_cfg.get('field1', '')
    f2 = name_cfg.get('field2', '')
    f3 = name_cfg.get('field3', '')
    f1_on = name_cfg.get('field1_enabled', True)
    f2_on = name_cfg.get('field2_enabled', True)
    f3_on = name_cfg.get('field3_enabled', False)
    layout = name_cfg.get('layout_rows', 1)
    if layout == 3:
        rows = []
        for f, on in [(f1, f1_on), (f2, f2_on), (f3, f3_on)]:
            if f and on:
                rows.append({'enabled': True,
                             'fields': [f, '', ''], 'suffix': ''})
        return rows or [{'enabled': True, 'fields': ['', '', ''],
                         'suffix': ''}]
    if layout == 2:
        rows = []
        r1 = [f for f, on in [(f1, f1_on), (f2, f2_on)] if f and on]
        if r1:
            while len(r1) < 3:
                r1.append('')
            rows.append({'enabled': True, 'fields': r1, 'suffix': ''})
        if f3 and f3_on:
            rows.append({'enabled': True, 'fields': [f3, '', ''],
                         'suffix': ''})
        return rows or [{'enabled': True, 'fields': ['', '', ''],
                         'suffix': ''}]
    # layout == 1: all on one row
    row_fields = [f for f, on in [(f1, f1_on), (f2, f2_on),
                                  (f3, f3_on)] if f and on]
    while len(row_fields) < 3:
        row_fields.append('')
    return [{'enabled': True, 'fields': row_fields, 'suffix': ''}]


def _old_state_to_rows(state):
    """Convert old UI state (f1_field/f2_field/…) → new rows list."""
    return _old_name_cfg_to_rows({
        'field1': state.get('f1_field', ''),
        'field1_enabled': state.get('f1_checked', True),
        'field2': state.get('f2_field', ''),
        'field2_enabled': state.get('f2_checked', False),
        'field3': state.get('f3_field', ''),
        'field3_enabled': state.get('f3_checked', False),
        'layout_rows': state.get('layout_rows', 1),
    })


# ── Dynamic label row widget ──────────────────────────

class _LabelRowWidget(QWidget):
    """One label line: [☑] [F▼]─[F▼]─[F▼] [+F][-F]  Suffix:[ha]  [×]

    Multiple of these make up the label editor in _GeomConfigPanel.
    """
    from qgis.PyQt.QtCore import pyqtSignal as _pSig
    changed = _pSig()
    remove_requested = _pSig(object)

    _MIN_FIELDS = 1
    _DEF_FIELDS = 3

    def __init__(self, fields, parent=None):
        super().__init__(parent)
        self._fields = list(fields)
        self._combos = []   # QComboBox list
        self._seps = []     # separator QLabel list
        self._setup_ui()

    def _setup_ui(self):
        outer = QHBoxLayout(self)
        outer.setContentsMargins(2, 1, 2, 1)
        outer.setSpacing(3)

        self.chk = QCheckBox()
        self.chk.setChecked(True)
        self.chk.setFixedWidth(18)
        self.chk.toggled.connect(self.changed)
        outer.addWidget(self.chk)

        self._fc = QWidget()
        self._fc_ly = QHBoxLayout(self._fc)
        self._fc_ly.setContentsMargins(0, 0, 0, 0)
        self._fc_ly.setSpacing(2)
        outer.addWidget(self._fc, 1)

        for lbl_txt, tip, slot, w in [
            ('+F', 'Thêm trường vào dòng', self._on_add, 28),
            ('-F', 'Bỏ trường cuối', self._on_rem, 28),
        ]:
            b = QPushButton(lbl_txt)
            b.setFixedSize(w, 22)
            b.setToolTip(tip)
            b.clicked.connect(slot)
            outer.addWidget(b)

        outer.addWidget(QLabel('Suffix:'))
        self.txt_suffix = QLineEdit()
        self.txt_suffix.setFixedSize(48, 22)
        self.txt_suffix.setPlaceholderText('ha')
        self.txt_suffix.textChanged.connect(self.changed)
        outer.addWidget(self.txt_suffix)

        # Per-row colour button
        self._row_color = ''  # empty = inherit from layer label color
        self.btn_row_color = QPushButton('■')
        self.btn_row_color.setFixedSize(28, 22)
        self.btn_row_color.setToolTip(
            'Màu dòng này (trống = dùng màu chung)')
        self._apply_row_color_btn()
        self.btn_row_color.clicked.connect(self._pick_row_color)
        outer.addWidget(self.btn_row_color)

        self.btn_del = QPushButton('×')
        self.btn_del.setFixedSize(22, 22)
        self.btn_del.setStyleSheet(
            'QPushButton{color:red;font-weight:bold;border:none}'
            'QPushButton:hover{background:#FFE0E0}')
        self.btn_del.setToolTip('Xóa dòng này')
        self.btn_del.clicked.connect(
            lambda: self.remove_requested.emit(self))

        outer.addWidget(self.btn_del)

        for _ in range(self._DEF_FIELDS):
            self._append_combo()

    # ── combo helpers ─────────────────────────────────

    def _make_combo(self, value=''):
        cbo = QComboBox()
        cbo.setFixedHeight(22)
        cbo.addItem('')
        cbo.addItems(self._fields)
        if value:
            i = cbo.findText(value)
            if i >= 0:
                cbo.setCurrentIndex(i)
        cbo.currentIndexChanged.connect(self.changed)
        return cbo

    def _append_combo(self, value=''):
        if self._combos:
            sep = QLabel('─')
            sep.setFixedWidth(10)
            sep.setAlignment(QtCompat.AlignCenter)
            self._fc_ly.addWidget(sep)
            self._seps.append(sep)
        cbo = self._make_combo(value)
        self._combos.append(cbo)
        self._fc_ly.addWidget(cbo)

    def _on_add(self):
        self._append_combo()
        self.changed.emit()

    def _on_rem(self):
        if len(self._combos) <= self._MIN_FIELDS:
            return
        cbo = self._combos.pop()
        cbo.blockSignals(True)
        self._fc_ly.removeWidget(cbo)
        cbo.deleteLater()
        if self._seps:
            sep = self._seps.pop()
            self._fc_ly.removeWidget(sep)
            sep.deleteLater()
        self.changed.emit()

    # ── row colour helpers ─────────────────────────────

    def _apply_row_color_btn(self):
        """Update button background to reflect current row colour."""
        if self._row_color:
            # Invert foreground for readability
            fg = '#000000' if self._row_color.upper() in (
                '#FFFFFF', '#FFFF00', '#00FFFF') else '#ffffff'
            self.btn_row_color.setStyleSheet(
                f'QPushButton{{background:{self._row_color};'
                f'color:{fg};border:1px solid #888}}')
        else:
            self.btn_row_color.setStyleSheet(
                'QPushButton{background:#e0e0e0;color:#666;'
                'border:1px solid #aaa;font-size:10px}')
            self.btn_row_color.setText('□')  # hollow = inherit

    def _pick_row_color(self):
        from qgis.PyQt.QtWidgets import QColorDialog, QMenu, QAction
        menu = QMenu(self)
        act_pick = QAction('🎨 Chọn màu...', menu)
        act_reset = QAction('♻ Reset (dùng màu chung)', menu)
        menu.addAction(act_pick)
        menu.addAction(act_reset)

        def do_pick():
            from qgis.PyQt.QtGui import QColor
            init = QColor(self._row_color) if self._row_color else QColor('#ffffff')
            c = QColorDialog.getColor(init, self, 'Chọn màu dòng')
            if c.isValid():
                self._row_color = c.name().upper()
                self.btn_row_color.setText('■')
                self._apply_row_color_btn()
                self.changed.emit()

        def do_reset():
            self._row_color = ''
            self.btn_row_color.setText('□')
            self._apply_row_color_btn()
            self.changed.emit()

        act_pick.triggered.connect(do_pick)
        act_reset.triggered.connect(do_reset)
        menu.exec(self.btn_row_color.mapToGlobal(
            self.btn_row_color.rect().bottomLeft()))

    def get_row_color(self):
        """Return per-row colour hex string, or '' to inherit."""
        return self._row_color

    # ── public interface ──────────────────────────────

    def update_fields(self, fields):
        """Refresh the available field list in all combos."""
        self._fields = list(fields)
        for cbo in self._combos:
            curr = cbo.currentText()
            cbo.blockSignals(True)
            cbo.clear()
            cbo.addItem('')
            cbo.addItems(fields)
            idx = cbo.findText(curr)
            cbo.setCurrentIndex(idx if idx >= 0 else 0)
            cbo.blockSignals(False)

    def get_row_data(self):
        return {
            'enabled': self.chk.isChecked(),
            'fields': [c.currentText() for c in self._combos],
            'suffix': self.txt_suffix.text().strip(),
            'color': self._row_color,   # '' = inherit layer label color
        }

    def load_row_data(self, data, fields=None):
        if fields is not None:
            self._fields = list(fields)
        # Clear combos + separators
        for w in self._combos + self._seps:
            w.blockSignals(True)
            self._fc_ly.removeWidget(w)
            w.deleteLater()
        self._combos.clear()
        self._seps.clear()

        self.chk.blockSignals(True)
        self.txt_suffix.blockSignals(True)
        self.chk.setChecked(data.get('enabled', True))
        self.txt_suffix.setText(data.get('suffix', ''))

        # Restore row colour
        self._row_color = data.get('color', '')
        self.btn_row_color.setText('■' if self._row_color else '□')
        self._apply_row_color_btn()

        flds = list(data.get('fields', []))
        while len(flds) < self._DEF_FIELDS:
            flds.append('')
        for f in flds:
            self._append_combo(f)

        self.chk.blockSignals(False)
        self.txt_suffix.blockSignals(False)

    def build_preview(self, sample, sep):
        """Return preview string for this row using sample data dict."""
        if not self.chk.isChecked():
            return ''
        parts = []
        for cbo in self._combos:
            fname = cbo.currentText()
            if not fname:
                continue
            v = str(sample.get(fname, ''))
            if v not in ('', 'NULL', 'None', 'null'):
                parts.append(v)
        if not parts:
            return ''
        text = sep.join(parts)
        suffix = self.txt_suffix.text().strip()
        if suffix:
            text += f' {suffix}'
        return text


# ── Geometry-type config panel ────────────────────────

class _GeomConfigPanel(QWidget):
    """Config panel for one geometry type with per-layer dropdown.

    Features:
      - Per-layer config save / restore via dropdown
      - Popup toggle (disable popup while keeping style)
      - Highlight column (★) with condition for conditional formatting
    """

    def __init__(self, geom_key, on_change=None, parent=None):
        super().__init__(parent)
        self.geom_key = geom_key
        self._fields = []
        self._sample_data = {}
        self._on_change = on_change
        self._loading = False
        # Per-layer storage
        self._layer_keys = []
        self._layer_names = {}
        self._layer_fields = {}
        self._layer_samples = {}
        self._layer_states = {}
        self._current_key = None
        self._setup_ui()
        self._connect_signals()

    # ── UI construction ───────────────────────────────

    def _setup_ui(self):
        scroll = QScrollArea()
        scroll.setWidgetResizable(True)
        scroll.setFrameShape(QFrame.Shape.NoFrame)
        inner = QWidget()
        ly = QVBoxLayout(inner)
        scroll.setWidget(inner)
        outer = QVBoxLayout(self)
        outer.setContentsMargins(0, 0, 0, 0)
        outer.addWidget(scroll)

        # 0 — Layer selector
        gp_layer = QGroupBox("Layer")
        layer_ly = QHBoxLayout()
        layer_ly.addWidget(QLabel("Chọn layer:"))
        self.cbo_layer = QComboBox()
        self.cbo_layer.setMinimumWidth(200)
        self.cbo_layer.setStyleSheet(
            "QComboBox{font-weight:bold;padding:3px}")
        layer_ly.addWidget(self.cbo_layer, 1)
        gp_layer.setLayout(layer_ly)
        ly.addWidget(gp_layer)

        # 1 — Name settings (dynamic multi-field rows)
        gp_name = QGroupBox('Nhãn (Label)')
        name_main = QVBoxLayout()

        # Top bar: Sep / Size / Color / + Thêm dòng
        ctrl_row = QHBoxLayout()
        ctrl_row.addWidget(QLabel('Sep:'))
        self.txt_sep = QLineEdit(' - ')
        self.txt_sep.setFixedWidth(50)
        self.txt_sep.setToolTip(
            'Ký tự nối các trường trong cùng 1 dòng')
        ctrl_row.addWidget(self.txt_sep)
        ctrl_row.addWidget(QLabel('Size:'))
        self.spn_size = QSpinBox()
        self.spn_size.setRange(8, 72)
        self.spn_size.setValue(12)
        ctrl_row.addWidget(self.spn_size)
        self.btn_name_clr = QPushButton('#FFFFFF')
        self.btn_name_clr.setFixedWidth(70)
        self.btn_name_clr.setStyleSheet(
            'background:#FFFFFF;color:black')
        ctrl_row.addWidget(self.btn_name_clr)
        ctrl_row.addStretch()
        self.btn_add_label_row = QPushButton('+ Thêm dòng')
        self.btn_add_label_row.setFixedHeight(24)
        self.btn_add_label_row.setToolTip(
            'Thêm một dòng nhãn mới')
        self.btn_add_label_row.clicked.connect(
            self._add_label_row)
        ctrl_row.addWidget(self.btn_add_label_row)
        name_main.addLayout(ctrl_row)

        # Dynamic label rows container
        self._label_rows = []
        self._label_rows_widget = QWidget()
        self._label_rows_ly = QVBoxLayout(
            self._label_rows_widget)
        self._label_rows_ly.setContentsMargins(2, 2, 2, 2)
        self._label_rows_ly.setSpacing(2)
        name_main.addWidget(self._label_rows_widget)

        # Start with one default row
        self._add_label_row_silent()

        gp_name.setLayout(name_main)
        ly.addWidget(gp_name)

        # 2 — Popup fields
        gp_popup = QGroupBox("Popup (Fields)")
        p_ly = QVBoxLayout()

        # Popup toggle
        self.chk_popup = QCheckBox("Bật Popup thông tin")
        self.chk_popup.setChecked(True)
        self.chk_popup.setStyleSheet(
            "font-weight:bold;color:#1B5E20")
        p_ly.addWidget(self.chk_popup)

        btn_row = QHBoxLayout()
        self.btn_chk_all = QPushButton("✅ All")
        self.btn_chk_all.setFixedWidth(70)
        self.btn_chk_all.clicked.connect(self._check_all)
        self.btn_unchk = QPushButton("❎ None")
        self.btn_unchk.setFixedWidth(70)
        self.btn_unchk.clicked.connect(self._uncheck_all)
        btn_row.addWidget(self.btn_chk_all)
        btn_row.addWidget(self.btn_unchk)
        btn_row.addStretch()
        self.btn_up = QPushButton("↑")
        self.btn_up.setFixedWidth(30)
        self.btn_up.clicked.connect(self._move_up)
        self.btn_down = QPushButton("↓")
        self.btn_down.setFixedWidth(30)
        self.btn_down.clicked.connect(self._move_down)
        btn_row.addWidget(self.btn_up)
        btn_row.addWidget(self.btn_down)
        p_ly.addLayout(btn_row)

        # Table: 5 columns — √, Field, Alias, Unit, ★
        self.tbl_fields = QTableWidget(0, 5)
        self.tbl_fields.setHorizontalHeaderLabels(
            ["√", "Field", "Alias", "Unit", "★"])
        hdr = self.tbl_fields.horizontalHeader()
        hdr.setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
        hdr.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
        hdr.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
        hdr.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
        hdr.setSectionResizeMode(4, QHeaderView.ResizeMode.Fixed)
        self.tbl_fields.setColumnWidth(0, 30)
        self.tbl_fields.setColumnWidth(4, 30)
        self.tbl_fields.setMinimumHeight(150)
        p_ly.addWidget(self.tbl_fields)

        # Highlight condition row
        hl_row = QHBoxLayout()
        self.chk_highlight = QCheckBox("Highlight:")
        self.chk_highlight.setStyleSheet(
            "font-weight:bold;color:#C62828")
        hl_row.addWidget(self.chk_highlight)
        hl_row.addWidget(QLabel("Khi"))
        self.cbo_hl_op = QComboBox()
        self.cbo_hl_op.addItems([">", ">=", "<", "<=", "=", "!="])
        self.cbo_hl_op.setFixedWidth(55)
        hl_row.addWidget(self.cbo_hl_op)
        self.txt_hl_val = QLineEdit("1")
        self.txt_hl_val.setFixedWidth(80)
        self.txt_hl_val.setPlaceholderText("Giá trị")
        hl_row.addWidget(self.txt_hl_val)
        hl_row.addWidget(QLabel("→"))
        self.lbl_hl_style = QLabel(
            " <b style='color:#0000CC;"
            "background:#FFE0E0;padding:2px 6px'>"
            "Bôi đậm</b> ")
        hl_row.addWidget(self.lbl_hl_style)
        hl_row.addStretch()
        p_ly.addLayout(hl_row)

        gp_popup.setLayout(p_ly)
        ly.addWidget(gp_popup)

        # 3 — Style (geometry-dependent)
        gp_style = QGroupBox("Style")
        s_ly = QHBoxLayout()

        if self.geom_key == 'point':
            s_ly.addWidget(QLabel("Icon:"))
            self.btn_icon_clr = QPushButton("#FF4444")
            self.btn_icon_clr.setStyleSheet("background:#FF4444")
            s_ly.addWidget(self.btn_icon_clr)
            s_ly.addWidget(QLabel("Scale:"))
            self.spn_icon_scale = QDoubleSpinBox()
            self.spn_icon_scale.setRange(0.3, 5.0)
            self.spn_icon_scale.setSingleStep(0.1)
            self.spn_icon_scale.setValue(1.0)
            s_ly.addWidget(self.spn_icon_scale)

        elif self.geom_key == 'line':
            s_ly.addWidget(QLabel("Color:"))
            self.btn_line_clr = QPushButton("#0000FF")
            self.btn_line_clr.setStyleSheet("background:#0000FF")
            s_ly.addWidget(self.btn_line_clr)
            s_ly.addWidget(QLabel("Width:"))
            self.spn_line_w = QSpinBox()
            self.spn_line_w.setRange(1, 20)
            self.spn_line_w.setValue(2)
            s_ly.addWidget(self.spn_line_w)

        else:  # polygon
            s_ly.addWidget(QLabel("Border:"))
            self.btn_border = QPushButton("#00FF00")
            self.btn_border.setStyleSheet("background:#00FF00")
            s_ly.addWidget(self.btn_border)
            s_ly.addWidget(QLabel("W:"))
            self.spn_bw = QSpinBox()
            self.spn_bw.setRange(1, 20)
            self.spn_bw.setValue(2)
            s_ly.addWidget(self.spn_bw)
            s_ly.addWidget(QLabel("Fill:"))
            self.btn_fill = QPushButton("#00FF00")
            self.btn_fill.setStyleSheet("background:#00FF00")
            s_ly.addWidget(self.btn_fill)
            s_ly.addWidget(QLabel("Op:"))
            self.sld_op = QSlider(QtCompat.Horizontal)
            self.sld_op.setRange(0, 100)
            self.sld_op.setValue(50)
            s_ly.addWidget(self.sld_op)

        gp_style.setLayout(s_ly)
        ly.addWidget(gp_style)

        # 4 — Header
        gp_hdr = QGroupBox("Header Popup")
        h_ly = QHBoxLayout()
        h_ly.addWidget(QLabel("Title:"))
        self.txt_header = QLineEdit("Thông tin")
        h_ly.addWidget(self.txt_header, 1)
        self.btn_hdr_bg = QPushButton("#1B5E20")
        self.btn_hdr_bg.setStyleSheet("background:#1B5E20")
        h_ly.addWidget(self.btn_hdr_bg)
        gp_hdr.setLayout(h_ly)
        ly.addWidget(gp_hdr)

        ly.addStretch()

    # ── Signal connections ────────────────────────────

    def _connect_signals(self):
        self.cbo_layer.currentIndexChanged.connect(
            self._on_layer_changed)

        widgets = [
            self.txt_sep, self.spn_size,
            self.txt_header, self.chk_popup,
            self.chk_highlight, self.cbo_hl_op,
            self.txt_hl_val,
        ]
        if self.geom_key == 'point':
            widgets.extend([self.spn_icon_scale])
        elif self.geom_key == 'line':
            widgets.extend([self.spn_line_w])
        else:
            widgets.extend([self.spn_bw, self.sld_op])

        for w in widgets:
            if hasattr(w, 'textChanged'):
                w.textChanged.connect(self._notify)
            if hasattr(w, 'valueChanged'):
                w.valueChanged.connect(self._notify)
            if hasattr(w, 'toggled'):
                w.toggled.connect(self._notify)
            if hasattr(w, 'currentIndexChanged'):
                w.currentIndexChanged.connect(self._notify)

        color_btns = [self.btn_name_clr, self.btn_hdr_bg]
        if self.geom_key == 'point':
            color_btns.append(self.btn_icon_clr)
        elif self.geom_key == 'line':
            color_btns.append(self.btn_line_clr)
        else:
            color_btns.extend([self.btn_border, self.btn_fill])
        for btn in color_btns:
            btn.clicked.connect(
                lambda checked, b=btn: (
                    self._pick(b), self._notify()))

        self.tbl_fields.itemChanged.connect(self._notify)

    # ── Label row management ──────────────────────────

    def _add_label_row_silent(self, data=None):
        """Add a row without emitting _notify (used in __init__)."""
        row = _LabelRowWidget(self._fields)
        if data:
            row.load_row_data(data)
        row.changed.connect(self._notify)
        row.remove_requested.connect(self._remove_label_row)
        self._label_rows.append(row)
        self._label_rows_ly.addWidget(row)

    def _add_label_row(self, data=None):
        """Add a new label row and trigger preview refresh."""
        self._add_label_row_silent(data)
        self._notify()

    def _remove_label_row(self, row_widget):
        """Remove a label row (minimum 1 preserved)."""
        if len(self._label_rows) <= 1:
            return
        self._label_rows.remove(row_widget)
        self._label_rows_ly.removeWidget(row_widget)
        row_widget.deleteLater()
        self._notify()

    def _clear_label_rows(self):
        """Remove all label row widgets."""
        while self._label_rows:
            r = self._label_rows.pop()
            self._label_rows_ly.removeWidget(r)
            r.deleteLater()

    # ── Layer switching ───────────────────────────────

    def _on_layer_changed(self, idx):
        if idx < 0 or self._loading:
            return
        if idx >= len(self._layer_keys):
            return
        self._loading = True
        try:
            self._save_current()
            self._current_key = self._layer_keys[idx]
            self._load_layer(self._current_key)
        finally:
            self._loading = False

    def _save_current(self):
        if self._current_key and not self._loading:
            self._layer_states[self._current_key] = (
                self._save_state())

    def _save_state(self):
        state = {
            'label_rows': [r.get_row_data()
                           for r in self._label_rows],
            'separator': self.txt_sep.text(),
            'font_size': self.spn_size.value(),
            'name_color': self.btn_name_clr.text(),
            'header_title': self.txt_header.text(),
            'header_bg': self.btn_hdr_bg.text(),
            'popup_enabled': self.chk_popup.isChecked(),
            'highlight_enabled': self.chk_highlight.isChecked(),
            'highlight_op': self.cbo_hl_op.currentText(),
            'highlight_val': self.txt_hl_val.text(),
            'fields': [],
        }
        for i in range(self.tbl_fields.rowCount()):
            chk = self.tbl_fields.cellWidget(i, 0)
            fi = self.tbl_fields.item(i, 1)
            ai = self.tbl_fields.item(i, 2)
            si = self.tbl_fields.item(i, 3)
            hl = self.tbl_fields.cellWidget(i, 4)
            state['fields'].append({
                'checked': chk.isChecked() if chk else True,
                'field': fi.text() if fi else '',
                'alias': ai.text() if ai else '',
                'unit': si.text() if si else '',
                'highlight': hl.isChecked() if hl else False,
            })
        if self.geom_key == 'point':
            state['icon_color'] = self.btn_icon_clr.text()
            state['icon_scale'] = self.spn_icon_scale.value()
        elif self.geom_key == 'line':
            state['line_color'] = self.btn_line_clr.text()
            state['line_width'] = self.spn_line_w.value()
        else:
            state['border_color'] = self.btn_border.text()
            state['border_width'] = self.spn_bw.value()
            state['fill_color'] = self.btn_fill.text()
            state['fill_opacity'] = self.sld_op.value()
        return state

    def _load_layer(self, key):
        self._loading = True
        fields = self._layer_fields.get(key, [])
        sample = self._layer_samples.get(key, {})
        state = self._layer_states.get(key)
        self._sample_data = sample
        self._fields = list(fields)

        # Block non-label widgets
        _block = [self.tbl_fields, self.txt_sep, self.spn_size,
                  self.txt_header, self.chk_popup, self.chk_highlight,
                  self.cbo_hl_op, self.txt_hl_val]
        for w in _block:
            w.blockSignals(True)

        if state:
            # ── Label rows ──────────────────────────────
            rows_data = state.get('label_rows')
            if rows_data is None:
                rows_data = _old_state_to_rows(state)
            self._clear_label_rows()
            for rd in (rows_data or []):
                row = _LabelRowWidget(fields)
                row.load_row_data(rd)
                row.changed.connect(self._notify)
                row.remove_requested.connect(
                    self._remove_label_row)
                self._label_rows.append(row)
                self._label_rows_ly.addWidget(row)
            if not self._label_rows:
                self._add_label_row_silent()
            else:
                for r in self._label_rows:
                    r.update_fields(fields)

            # ── Scalar settings ──────────────────────────
            self.txt_sep.setText(state.get('separator', ' - '))
            self.spn_size.setValue(state.get('font_size', 12))
            nc = state.get('name_color', '#000000')
            self.btn_name_clr.setText(nc)
            self.btn_name_clr.setStyleSheet(
                f'background-color:{nc}')
            self.txt_header.setText(
                state.get('header_title', 'Thông tin'))
            hbg = state.get('header_bg', '#1B5E20')
            self.btn_hdr_bg.setText(hbg)
            self.btn_hdr_bg.setStyleSheet(
                f'background-color:{hbg}')
            self.chk_popup.setChecked(
                state.get('popup_enabled', True))
            self.chk_highlight.setChecked(
                state.get('highlight_enabled', False))
            op = state.get('highlight_op', '>')
            oi = self.cbo_hl_op.findText(op)
            if oi >= 0:
                self.cbo_hl_op.setCurrentIndex(oi)
            self.txt_hl_val.setText(
                state.get('highlight_val', '1'))

            # ── Style ────────────────────────────────────
            if self.geom_key == 'point':
                ic = state.get('icon_color', '#FF4444')
                self.btn_icon_clr.setText(ic)
                self.btn_icon_clr.setStyleSheet(
                    f'background-color:{ic}')
                self.spn_icon_scale.setValue(
                    state.get('icon_scale', 1.0))
            elif self.geom_key == 'line':
                lc = state.get('line_color', '#0000FF')
                self.btn_line_clr.setText(lc)
                self.btn_line_clr.setStyleSheet(
                    f'background-color:{lc}')
                self.spn_line_w.setValue(
                    state.get('line_width', 2))
            else:
                bc = state.get('border_color', '#FF0000')
                self.btn_border.setText(bc)
                self.btn_border.setStyleSheet(
                    f'background-color:{bc}')
                self.spn_bw.setValue(
                    state.get('border_width', 2))
                fc = state.get('fill_color', '#00FF00')
                self.btn_fill.setText(fc)
                self.btn_fill.setStyleSheet(
                    f'background-color:{fc}')
                self.sld_op.setValue(
                    state.get('fill_opacity', 50))

            # ── Popup field table ─────────────────────────
            self.tbl_fields.setRowCount(0)
            saved_fnames = set()
            for sf in state.get('fields', []):
                fn = sf.get('field', '')
                if fn not in fields:
                    continue
                saved_fnames.add(fn)
                r = self.tbl_fields.rowCount()
                self.tbl_fields.insertRow(r)
                chk = QCheckBox()
                chk.setChecked(sf.get('checked', True))
                chk.stateChanged.connect(self._notify)
                self.tbl_fields.setCellWidget(r, 0, chk)
                self.tbl_fields.setItem(
                    r, 1, QTableWidgetItem(fn))
                self.tbl_fields.setItem(
                    r, 2, QTableWidgetItem(
                        sf.get('alias', fn)))
                self.tbl_fields.setItem(
                    r, 3, QTableWidgetItem(
                        sf.get('unit', '')))
                hl = QCheckBox()
                hl.setChecked(sf.get('highlight', False))
                hl.stateChanged.connect(self._notify)
                self.tbl_fields.setCellWidget(r, 4, hl)
            for fn in fields:
                if fn not in saved_fnames:
                    self._add_field_row(fn)
        else:
            # Default: one row, all fields shown
            self.chk_popup.setChecked(True)
            self.chk_highlight.setChecked(False)
            self._clear_label_rows()
            self._add_label_row_silent()
            for r in self._label_rows:
                r.update_fields(fields)
            self.tbl_fields.setRowCount(0)
            for fn in fields:
                self._add_field_row(fn)

        # Unblock
        for w in _block:
            w.blockSignals(False)

        self._loading = False
        self._notify()

    def _add_field_row(self, fn, checked=True, highlight=False):
        """Add a row to the fields table."""
        r = self.tbl_fields.rowCount()
        self.tbl_fields.insertRow(r)
        chk = QCheckBox()
        chk.setChecked(checked)
        chk.stateChanged.connect(self._notify)
        self.tbl_fields.setCellWidget(r, 0, chk)
        self.tbl_fields.setItem(
            r, 1, QTableWidgetItem(fn))
        self.tbl_fields.setItem(
            r, 2, QTableWidgetItem(fn))
        self.tbl_fields.setItem(
            r, 3, QTableWidgetItem(""))
        hl = QCheckBox()
        hl.setChecked(highlight)
        hl.stateChanged.connect(self._notify)
        self.tbl_fields.setCellWidget(r, 4, hl)

    def _notify(self):
        if self._loading:
            return
        self._update_name_label()
        if self._on_change:
            self._on_change()

    def _update_name_label(self):
        # Preview text label removed (Live Preview on the right is sufficient)
        pass  # nosec B110

    # ── Public methods ────────────────────────────────

    def build_name_text(self):
        """Build label preview as \\n-separated string (all lines merged)."""
        sep = self.txt_sep.text()
        return '\n'.join(
            t for t, _ in self.build_label_lines(sep) if t
        )

    def build_label_lines(self, sep=None):
        """Return list of (text, css_color) for each enabled label row."""
        if sep is None:
            sep = self.txt_sep.text()
        default_clr = self.btn_name_clr.text() or '#ffffff'
        result = []
        for row in self._label_rows:
            text = row.build_preview(self._sample_data, sep)
            if text:
                clr = row.get_row_color() or default_clr
                result.append((text, clr))
        return result

    def set_layers(self, layers_info):
        """Update layer list, preserving existing per-layer configs.

        Bug-fixed order of operations:
        1. Save current UI state.
        2. Build new key/field/sample maps FIRST (before clearing anything).
        3. Prune states for keys no longer present.
        4. Assign the new lists.
        5. Repopulate combo and load.

        The old code did ``self._layer_keys = []`` before step 3, so the
        membership test ``old not in self._layer_keys`` was always True
        and every saved per-layer config was deleted.
        """
        self._save_current()

        # ── 1. Build new structures BEFORE touching instance vars ──
        new_keys    = []
        new_fields  = {}
        new_names   = {}
        new_samples = {}
        for key, name, fields, sample in layers_info:
            new_keys.append(key)
            new_fields[key]  = fields
            new_names[key]   = name
            new_samples[key] = sample

        # ── 2. Remove stale states (not in the INCOMING list) ──────
        for old in list(self._layer_states.keys()):
            if old not in new_keys:
                del self._layer_states[old]
                self._layer_names.pop(old, None)

        # ── 3. Apply new structures ─────────────────────────────────
        self._layer_keys    = new_keys
        self._layer_fields  = new_fields
        self._layer_names.update(new_names)
        self._layer_samples = new_samples

        prev_key = self._current_key
        self.cbo_layer.blockSignals(True)
        self.cbo_layer.clear()
        for key in self._layer_keys:
            self.cbo_layer.addItem(self._layer_names[key])
        if prev_key in self._layer_keys:
            idx = self._layer_keys.index(prev_key)
            self.cbo_layer.setCurrentIndex(idx)
        self.cbo_layer.blockSignals(False)

        if self._layer_keys:
            idx = self.cbo_layer.currentIndex()
            if idx < 0:
                idx = 0
            self._current_key = self._layer_keys[idx]
            self._load_layer(self._current_key)
        else:
            self._current_key = None
            self._sample_data = {}
            self._fields = []
            self.tbl_fields.setRowCount(0)
            self._notify()

    def get_all_layer_configs(self):
        self._save_current()
        result = []
        for key in self._layer_keys:
            state = self._layer_states.get(key)
            fields = self._layer_fields.get(key, [])
            if state:
                cfg = self._state_to_config(state)
            else:
                cfg = self._default_config(fields)
            result.append((key, cfg))
        return result

    def load_all_layer_configs(self, saved_list):
        if not saved_list:
            return

        import os
        saved_dict = {}
        saved_by_basename = {}
        for k, cfg in saved_list:
            saved_dict[k] = cfg
            saved_by_basename[os.path.basename(k)] = cfg

        for current_key in self._layer_keys:
            cfg = None
            if current_key in saved_dict:
                cfg = saved_dict[current_key]
            elif os.path.basename(current_key) in saved_by_basename:
                cfg = saved_by_basename[os.path.basename(current_key)]
            elif len(saved_list) == 1:
                cfg = saved_list[0][1]

            if cfg:
                self._layer_states[current_key] = self._config_to_state(
                    cfg, self._layer_fields.get(current_key, []))

        if self._current_key in self._layer_keys:
            self._load_layer(self._current_key)

    def _config_to_state(self, cfg, current_fields):
        name_cfg = cfg.get('name_fields', {})
        # Support both new (rows) and old (field1/2/3) formats
        if 'rows' in name_cfg:
            label_rows = name_cfg['rows']
        else:
            label_rows = _old_name_cfg_to_rows(name_cfg)
        state = {
            'label_rows': label_rows,
            'separator': name_cfg.get('separator', ' - '),
            'font_size': name_cfg.get('font_size', 12),
            'name_color': name_cfg.get('font_color', '#000000'),
            'popup_enabled': cfg.get('popup_enabled', True),
            'header_title': cfg.get('header', {}).get(
                'title', 'Thông tin'),
            'header_bg': cfg.get('header', {}).get(
                'bg_color', '#1B5E20'),
        }

        hl_cfg = cfg.get('row_highlights', {})
        state['highlight_enabled'] = hl_cfg.get('enabled', False)
        hl_rules = hl_cfg.get('rules', [])
        if hl_rules:
            state['highlight_op'] = hl_rules[0].get('operator', '>')
            state['highlight_val'] = hl_rules[0].get('value', '1')
        else:
            state['highlight_op'] = '>'
            state['highlight_val'] = '1'

        desc_cfgs = {
            d.get('field'): d
            for d in cfg.get('description_fields', [])}
        fields_state = []
        hl_fields = [r.get('field') for r in hl_rules]

        for f in current_fields:
            if f in desc_cfgs:
                d = desc_cfgs[f]
                fields_state.append({
                    'checked': d.get('checked', True),
                    'field': f,
                    'alias': d.get('alias', f),
                    'unit': d.get('suffix', ''),
                    'highlight': f in hl_fields,
                    'order': d.get('order', 999)
                })
            else:
                fields_state.append({
                    'checked': False, 'field': f,
                    'alias': f, 'unit': '',
                    'highlight': False, 'order': 999
                })

        fields_state.sort(key=lambda x: x['order'])
        state['fields'] = fields_state

        if self.geom_key == 'point':
            pt_cfg = cfg.get('point_style', {})
            state['icon_color'] = pt_cfg.get(
                'icon_color', '#FF4444')
            state['icon_scale'] = pt_cfg.get('icon_scale', 1.0)
        elif self.geom_key == 'line':
            ln_cfg = cfg.get('line_style', {})
            state['line_color'] = ln_cfg.get(
                'line_color', '#0000FF')
            state['line_width'] = ln_cfg.get('line_width', 2)
        else:
            pg_cfg = cfg.get('polygon_style', {})
            state['border_color'] = pg_cfg.get(
                'border_color', '#FF0000')
            state['border_width'] = pg_cfg.get(
                'border_width', 2)
            state['fill_color'] = pg_cfg.get(
                'fill_color', '#00FF00')
            state['fill_opacity'] = pg_cfg.get(
                'fill_opacity', 50)
        return state

    def _state_to_config(self, state):
        # New rows format
        label_rows = state.get('label_rows')
        if label_rows is None:
            # Convert legacy state
            label_rows = _old_state_to_rows(state)
        name_cfg = {
            'rows': label_rows,
            'separator': state.get('separator', ' - '),
            'font_size': state.get('font_size', 12),
            'font_color': state.get('name_color', '#000000'),
        }
        desc = []
        for i, sf in enumerate(state.get('fields', [])):
            if sf.get('checked', True):
                desc.append({
                    'field': sf.get('field', ''),
                    'alias': sf.get('alias', ''),
                    'suffix': sf.get('unit', ''),
                    'order': i,
                })

        # Build highlight rules from ★-marked fields
        hl_rules = []
        if state.get('highlight_enabled', False):
            op = state.get('highlight_op', '>')
            val = state.get('highlight_val', '1')
            for sf in state.get('fields', []):
                if sf.get('highlight', False):
                    hl_rules.append({
                        'field': sf.get('field', ''),
                        'operator': op,
                        'value': val,
                        'text_color': '#0000CC',
                        'bg_color': '#FFE0E0',
                        'bold': True,
                    })

        cfg = {
            'name_fields': name_cfg,
            'description_fields': desc,
            'popup_enabled': state.get('popup_enabled', True),
            'header': {
                'title': state.get(
                    'header_title', 'Thông tin'),
                'bg_color': state.get(
                    'header_bg', '#1B5E20'),
                'text_color': '#FFFFFF',
                'bold': True,
                'font_size': 14,
            },
            'row_highlights': {
                'enabled': bool(hl_rules),
                'rules': hl_rules,
            },
        }
        if self.geom_key == 'point':
            cfg['point_style'] = {
                'icon_color': state.get(
                    'icon_color', '#FF4444'),
                'icon_scale': state.get('icon_scale', 1.0),
            }
        elif self.geom_key == 'line':
            cfg['line_style'] = {
                'line_color': state.get(
                    'line_color', '#0000FF'),
                'line_width': state.get('line_width', 2),
            }
        else:
            cfg['polygon_style'] = {
                'border_color': state.get(
                    'border_color', '#FF0000'),
                'border_width': state.get(
                    'border_width', 2),
                'fill_color': state.get(
                    'fill_color', '#00FF00'),
                'fill_opacity': state.get(
                    'fill_opacity', 50),
            }
        return cfg

    def _default_config(self, fields):
        desc = [{'field': f, 'alias': f, 'suffix': '', 'order': i}
                for i, f in enumerate(fields)]
        row_fields = list(fields[:3]) if fields else []
        while len(row_fields) < 3:
            row_fields.append('')
        cfg = {
            'name_fields': {
                'rows': [{
                    'enabled': True,
                    'fields': row_fields,
                    'suffix': '',
                }],
                'separator': ' - ',
                'font_size': 12,
                'font_color': '#FFFFFF',
            },
            'description_fields': desc,
            'popup_enabled': True,
            'header': {
                'title': 'Thông tin',
                'bg_color': '#1B5E20',
                'text_color': '#FFFFFF',
                'bold': True,
                'font_size': 14,
            },
            'row_highlights': {'enabled': False, 'rules': []},
        }
        if self.geom_key == 'point':
            cfg['point_style'] = {
                'icon_color': '#FF4444', 'icon_scale': 1.0}
        elif self.geom_key == 'line':
            cfg['line_style'] = {
                'line_color': '#0000FF', 'line_width': 2}
        else:
            cfg['polygon_style'] = {
                'border_color': '#00FF00', 'border_width': 2,
                'fill_color': '#00FF00', 'fill_opacity': 10,
            }
        return cfg

    def get_config(self):
        """Return config for the currently displayed layer."""
        # Build name_fields in new rows format
        name_cfg = {
            'rows': [r.get_row_data()
                     for r in self._label_rows],
            'separator': self.txt_sep.text(),
            'font_size': self.spn_size.value(),
            'font_color': self.btn_name_clr.text(),
        }
        desc = []
        hl_rules = []
        for i in range(self.tbl_fields.rowCount()):
            chk = self.tbl_fields.cellWidget(i, 0)
            if chk and chk.isChecked():
                fi = self.tbl_fields.item(i, 1)
                ai = self.tbl_fields.item(i, 2)
                si = self.tbl_fields.item(i, 3)
                if fi and ai:
                    desc.append({
                        'field': fi.text(),
                        'alias': ai.text(),
                        'suffix': si.text() if si else '',
                        'order': i,
                    })
            # Highlight
            hl = self.tbl_fields.cellWidget(i, 4)
            if (hl and hl.isChecked() and  # noqa: W504
                    self.chk_highlight.isChecked()):
                fi = self.tbl_fields.item(i, 1)
                if fi:
                    hl_rules.append({
                        'field': fi.text(),
                        'operator': self.cbo_hl_op.currentText(),
                        'value': self.txt_hl_val.text(),
                        'text_color': '#0000CC',
                        'bg_color': '#FFE0E0',
                        'bold': True,
                    })
        cfg = {
            'name_fields': name_cfg,
            'description_fields': desc,
            'popup_enabled': self.chk_popup.isChecked(),
            'header': {
                'title': self.txt_header.text(),
                'bg_color': self.btn_hdr_bg.text(),
                'text_color': '#FFFFFF',
                'bold': True,
                'font_size': 14,
            },
            'row_highlights': {
                'enabled': bool(hl_rules),
                'rules': hl_rules,
            },
        }
        if self.geom_key == 'point':
            cfg['point_style'] = {
                'icon_color': self.btn_icon_clr.text(),
                'icon_scale': self.spn_icon_scale.value(),
            }
        elif self.geom_key == 'line':
            cfg['line_style'] = {
                'line_color': self.btn_line_clr.text(),
                'line_width': self.spn_line_w.value(),
            }
        else:
            cfg['polygon_style'] = {
                'border_color': self.btn_border.text(),
                'border_width': self.spn_bw.value(),
                'fill_color': self.btn_fill.text(),
                'fill_opacity': self.sld_op.value(),
            }
        return cfg

    def update_labels(self, lang):
        is_vi = lang == 'vi'
        for gp in self.findChildren(QGroupBox):
            t = gp.title()
            if t == 'Layer':
                pass  # nosec B110
            elif 'Label' in t or 'Nhãn' in t:
                gp.setTitle(
                    "Nhãn (Label)" if is_vi else "Label")
            elif 'Popup' in t and 'Header' not in t:
                gp.setTitle(
                    "Popup (Trường dữ liệu)" if is_vi
                    else "Popup (Fields)")
            elif 'Style' in t or 'Kiểu' in t:
                gp.setTitle(
                    "Kiểu dáng" if is_vi else "Style")
            elif 'Header' in t or 'Tiêu đề' in t:
                gp.setTitle(
                    "Tiêu đề Popup" if is_vi
                    else "Popup Header")
        for lbl in self.findChildren(QLabel):
            if lbl.text() in ('Chọn layer:',
                              'Select layer:'):
                lbl.setText(
                    "Chọn layer:" if is_vi
                    else "Select layer:")
        self.chk_popup.setText(
            "Bật Popup thông tin" if is_vi
            else "Enable Popup Info")
        self.chk_highlight.setText(
            "Highlight:" if is_vi
            else "Highlight:")

    # ── Private helpers ───────────────────────────────

    def _pick(self, btn):
        c = QColorDialog.getColor(QColor(btn.text()))
        if c.isValid():
            btn.setText(c.name())
            btn.setStyleSheet(
                f"background-color:{c.name()}")

    def _check_all(self):
        for i in range(self.tbl_fields.rowCount()):
            chk = self.tbl_fields.cellWidget(i, 0)
            if chk:
                chk.setChecked(True)

    def _uncheck_all(self):
        for i in range(self.tbl_fields.rowCount()):
            chk = self.tbl_fields.cellWidget(i, 0)
            if chk:
                chk.setChecked(False)

    def _move_up(self):
        row = self.tbl_fields.currentRow()
        if row <= 0:
            return
        self._swap_rows(row, row - 1)
        self.tbl_fields.setCurrentCell(row - 1, 0)
        self._notify()

    def _move_down(self):
        row = self.tbl_fields.currentRow()
        if row < 0 or row >= self.tbl_fields.rowCount() - 1:
            return
        self._swap_rows(row, row + 1)
        self.tbl_fields.setCurrentCell(row + 1, 0)
        self._notify()

    def _swap_rows(self, r1, r2):
        self.tbl_fields.blockSignals(True)
        chk1 = self.tbl_fields.cellWidget(r1, 0)
        chk2 = self.tbl_fields.cellWidget(r2, 0)
        c1 = chk1.isChecked() if chk1 else True
        c2 = chk2.isChecked() if chk2 else True
        hl1 = self.tbl_fields.cellWidget(r1, 4)
        hl2 = self.tbl_fields.cellWidget(r2, 4)
        h1 = hl1.isChecked() if hl1 else False
        h2 = hl2.isChecked() if hl2 else False
        vals1 = [self.tbl_fields.item(r1, c).text()
                 if self.tbl_fields.item(r1, c) else ''
                 for c in range(1, 4)]
        vals2 = [self.tbl_fields.item(r2, c).text()
                 if self.tbl_fields.item(r2, c) else ''
                 for c in range(1, 4)]
        new_c1 = QCheckBox()
        new_c1.setChecked(c2)
        new_c1.stateChanged.connect(self._notify)
        new_c2 = QCheckBox()
        new_c2.setChecked(c1)
        new_c2.stateChanged.connect(self._notify)
        self.tbl_fields.setCellWidget(r1, 0, new_c1)
        self.tbl_fields.setCellWidget(r2, 0, new_c2)
        new_h1 = QCheckBox()
        new_h1.setChecked(h2)
        new_h1.stateChanged.connect(self._notify)
        new_h2 = QCheckBox()
        new_h2.setChecked(h1)
        new_h2.stateChanged.connect(self._notify)
        self.tbl_fields.setCellWidget(r1, 4, new_h1)
        self.tbl_fields.setCellWidget(r2, 4, new_h2)
        for c in range(1, 4):
            self.tbl_fields.setItem(
                r1, c, QTableWidgetItem(vals2[c - 1]))
            self.tbl_fields.setItem(
                r2, c, QTableWidgetItem(vals1[c - 1]))
        self.tbl_fields.blockSignals(False)


# ── Main Merge Tab widget ─────────────────────────────

class MergeTabWidget(QWidget):
    """Complete Tab 3 for merging multiple SHP → 1 KML / KMZ."""

    GEOM_LABELS = {
        QgsWkbTypes.GeometryType.PointGeometry: ('point', '🔵 Điểm (Point)'),
        QgsWkbTypes.GeometryType.LineGeometry: ('line', '🟢 Đường (Line)'),
        QgsWkbTypes.GeometryType.PolygonGeometry: (
            'polygon', '🟡 Vùng (Polygon)'),
    }

    def __init__(self, lang='vi', parent=None):
        super().__init__(parent)
        self.lang = lang
        self._shp_paths = []
        self._shp_infos = []
        self._initialized = False
        self._bg_img = os.path.join(
            os.path.dirname(__file__),
            "google_maps_satellite_bg.png")
        self._placeholder_ly = QVBoxLayout(self)
        self._placeholder_ly.addWidget(
            QLabel("⏳ Nhấn vào tab để bắt đầu..."))
        self._placeholder_ly.addStretch()

    def ensure_initialized(self):
        if self._initialized:
            return
        self._initialized = True
        while self._placeholder_ly.count():
            item = self._placeholder_ly.takeAt(0)
            w = item.widget()
            if w:
                w.deleteLater()
        root_ly = self._placeholder_ly
        main_split = QSplitter(QtCompat.Horizontal)

        # ══════ LEFT ══════
        left = QWidget()
        left_ly = QVBoxLayout(left)
        left_ly.setContentsMargins(0, 0, 0, 0)

        gp_files = QGroupBox("1. Chọn file SHP")
        fl = QVBoxLayout()
        btn_row = QHBoxLayout()
        self.btn_add = QPushButton("➕ Thêm SHP")
        self.btn_add.clicked.connect(self._add_shp)
        self.btn_add_canvas = QPushButton("🗺 Lấy từ QGIS")
        self.btn_add_canvas.setStyleSheet(
            "QPushButton{background:#1565C0;color:#fff;"
            "font-weight:bold;padding:4px 10px;"
            "border-radius:3px}"
            "QPushButton:hover{background:#1976D2}")
        self.btn_add_canvas.clicked.connect(
            self._add_from_canvas)
        self.btn_remove = QPushButton("➖ Xóa")
        self.btn_remove.clicked.connect(self._remove_shp)
        self.btn_clear = QPushButton("🗑 Xóa hết")
        self.btn_clear.clicked.connect(self._clear_shp)
        btn_row.addWidget(self.btn_add)
        btn_row.addWidget(self.btn_add_canvas)
        btn_row.addWidget(self.btn_remove)
        btn_row.addWidget(self.btn_clear)
        btn_row.addStretch()
        fl.addLayout(btn_row)

        self.tbl_files = QTableWidget(0, 4)
        self.tbl_files.setHorizontalHeaderLabels(
            ['✓', 'File', 'Geometry', 'Path'])
        self.tbl_files.horizontalHeader().setSectionResizeMode(
            0, QHeaderView.ResizeMode.Fixed)
        self.tbl_files.setColumnWidth(0, 30)
        self.tbl_files.horizontalHeader().setSectionResizeMode(
            1, QHeaderView.ResizeMode.Stretch)
        self.tbl_files.horizontalHeader().setSectionResizeMode(
            2, QHeaderView.ResizeMode.ResizeToContents)
        self.tbl_files.horizontalHeader().setSectionResizeMode(
            3, QHeaderView.ResizeMode.Stretch)
        self.tbl_files.setMaximumHeight(120)
        self.tbl_files.clicked.connect(self._on_file_clicked)
        fl.addWidget(self.tbl_files)
        self.lbl_summary = QLabel("")
        self.lbl_summary.setStyleSheet(
            "color:#555;font-size:11px")
        fl.addWidget(self.lbl_summary)
        gp_files.setLayout(fl)
        left_ly.addWidget(gp_files)

        gp_cfg = QGroupBox(
            "2. Thiết lập theo loại đối tượng")
        cfg_ly = QVBoxLayout()
        self.geom_tabs = QTabWidget()
        self.panel_point = _GeomConfigPanel(
            'point', on_change=self._refresh_preview)
        self.panel_line = _GeomConfigPanel(
            'line', on_change=self._refresh_preview)
        self.panel_poly = _GeomConfigPanel(
            'polygon', on_change=self._refresh_preview)
        self.geom_tabs.addTab(
            self.panel_point, "🔵 Điểm (Point)")
        self.geom_tabs.addTab(
            self.panel_line, "🟢 Đường (Line)")
        self.geom_tabs.addTab(
            self.panel_poly, "🟡 Vùng (Polygon)")
        self.geom_tabs.currentChanged.connect(
            lambda: self._refresh_preview())
        cfg_ly.addWidget(self.geom_tabs)
        gp_cfg.setLayout(cfg_ly)
        left_ly.addWidget(gp_cfg, 1)
        main_split.addWidget(left)

        # ══════ RIGHT — LIVE PREVIEW ══════
        right = QWidget()
        r_ly = QVBoxLayout(right)
        r_ly.addWidget(QLabel("<b>🖼 LIVE PREVIEW</b>"))

        self._preview_frame = QFrame()
        self._preview_frame.setMinimumHeight(180)
        self._preview_frame.setFrameStyle(QFrame.Shape.StyledPanel)
        bg_path = self._bg_img.replace('\\', '/')
        self._preview_frame.setStyleSheet(
            f"background-image: url({bg_path});"
            "background-position: center;")
        self._preview_frame.paintEvent = self._paint_preview
        r_ly.addWidget(self._preview_frame)

        r_ly.addWidget(QLabel("<b>Popup Preview:</b>"))
        self._html_preview = QTextEdit()
        self._html_preview.setReadOnly(True)
        r_ly.addWidget(self._html_preview, 1)

        # Config buttons
        cfg_row = QHBoxLayout()
        self.btn_save_cfg = QPushButton('💾 Lưu cấu hình')
        self.btn_save_cfg.setStyleSheet(
            'QPushButton{background:#7B1FA2;color:#fff;font-weight:bold;'
            'padding:6px 12px;border-radius:4px}'
            'QPushButton:hover{background:#8E24AA}')
        self.btn_save_cfg.clicked.connect(self._save_config)
        cfg_row.addWidget(self.btn_save_cfg)

        self.btn_load_cfg = QPushButton('📂 Mở cấu hình')
        self.btn_load_cfg.setStyleSheet(
            'QPushButton{background:#00838F;color:#fff;font-weight:bold;'
            'padding:6px 12px;border-radius:4px}'
            'QPushButton:hover{background:#0097A7}')
        self.btn_load_cfg.clicked.connect(self._load_config)
        cfg_row.addWidget(self.btn_load_cfg)

        self.btn_reset_cfg = QPushButton('🔄 Reset')
        self.btn_reset_cfg.setStyleSheet(
            'QPushButton{background:#E65100;color:#fff;font-weight:bold;'
            'padding:6px 12px;border-radius:4px}'
            'QPushButton:hover{background:#EF6C00}')
        self.btn_reset_cfg.clicked.connect(self._reset_config)
        cfg_row.addWidget(self.btn_reset_cfg)
        cfg_row.addStretch()
        r_ly.addLayout(cfg_row)

        # ── Map scale row ─────────────────────────────────────────────────
        scale_row = QHBoxLayout()
        self.lbl_scale = QLabel("🗺️ Tỷ lệ bản đồ / Map Scale:")
        self.lbl_scale.setToolTip(
            "Tỷ lệ hiển thị để lọc layer theo scaleBasedVisibility.\n"
            "Map scale for scale-dependent layer visibility filtering.\n"
            "Chọn 'Tất cả' để xuất tất cả feature bất kể tỷ lệ.")
        scale_row.addWidget(self.lbl_scale)

        self.cmb_scale = QComboBox()
        _SCALES = [
            ("Tất cả / All", None),
            ("1 : 200", 200),
            ("1 : 500", 500),
            ("1 : 1 000", 1_000),
            ("1 : 2 000", 2_000),
            ("1 : 5 000", 5_000),
            ("1 : 10 000", 10_000),
            ("1 : 25 000", 25_000),
            ("1 : 50 000", 50_000),
            ("1 : 100 000", 100_000),
            ("1 : 250 000", 250_000),
            ("1 : 500 000", 500_000),
        ]
        for label, _ in _SCALES:
            self.cmb_scale.addItem(label)
        self.cmb_scale.setFixedWidth(160)
        self.cmb_scale.setToolTip(self.lbl_scale.toolTip())
        self._scale_values = [v for _, v in _SCALES]
        scale_row.addWidget(self.cmb_scale)
        scale_row.addStretch()
        r_ly.addLayout(scale_row)
        # ─────────────────────────────────────────────────────────────────

        self.btn_export = QPushButton("🚀 XUẤT KML / KMZ")
        self.btn_export.setMinimumHeight(45)
        self.btn_export.setStyleSheet(
            "background:#1B5E20;color:white;"
            "font-weight:bold;border-radius:5px;"
            "font-size:14px")
        self.btn_export.clicked.connect(self._export)
        r_ly.addWidget(self.btn_export)

        main_split.addWidget(right)
        main_split.setStretchFactor(0, 7)
        main_split.setStretchFactor(1, 3)
        root_ly.addWidget(main_split)

    # ── Preview ───────────────────────────────────────

    def _active_panel(self):
        idx = self.geom_tabs.currentIndex()
        return [self.panel_point, self.panel_line,
                self.panel_poly][idx]

    def _refresh_preview(self):
        if not self._initialized:
            return
        self._preview_frame.update()
        self._update_popup_preview()

    def _update_popup_preview(self):
        panel = self._active_panel()
        if not panel.chk_popup.isChecked():
            self._html_preview.setHtml(
                "<div style='text-align:center;"
                "color:#888;padding:20px'>"
                "<b>Popup đã tắt</b><br>"
                "Layer này chỉ hiển thị style.</div>")
            return
        if not panel._sample_data:
            self._html_preview.setHtml(
                "<i style='color:#999'>"
                "Chưa có dữ liệu mẫu.<br>"
                "Thêm file SHP để xem preview.</i>")
            return
        try:
            cfg = panel.get_config()
            html = HtmlTemplateBuilder(cfg).build(
                panel._sample_data)
            self._html_preview.setHtml(
                f"<div align='center'>{html}</div>")
        except Exception:
            self._html_preview.setHtml(
                "<i style='color:#c00'>Preview error</i>")

    def _paint_preview(self, event):
        panel = self._active_panel()
        painter = QPainter(self._preview_frame)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
        w = self._preview_frame.width()
        h = self._preview_frame.height()
        cx, cy = w // 2, h // 2
        gk = panel.geom_key
        if gk == 'polygon':
            self._paint_polygon(painter, panel, cx, cy)
        elif gk == 'line':
            self._paint_line(painter, panel, w, h)
        else:
            self._paint_point(painter, panel, cx, cy)
        try:
            label_lines = panel.build_label_lines()  # [(text, css_color), ...]
            if label_lines:
                sz = max(8, panel.spn_size.value())
                font = painter.font()
                font.setPointSize(sz)
                font.setBold(True)
                painter.setFont(font)
                fm = painter.fontMetrics()
                line_h = fm.height() + 2
                n_lines = len(label_lines)
                total_h = line_h * n_lines
                start_y = cy - total_h // 2 + fm.ascent()
                for i, (lt, css_color) in enumerate(label_lines):
                    clr = QColor(css_color) if css_color else QColor('#ffffff')
                    tw = fm.horizontalAdvance(lt)
                    tx = cx - tw // 2
                    ty = start_y + i * line_h
                    # Draw shadow/outline for readability on any background
                    painter.setPen(QColor(0, 0, 0, 180))
                    for dx, dy in [(-1, -1), (1, -1), (-1, 1), (1, 1)]:
                        painter.drawText(tx + dx, ty + dy, lt)
                    # Draw actual label in its row color
                    painter.setPen(clr)
                    painter.drawText(tx, ty, lt)
        except Exception:  # nosec B110
            pass  # nosec B110

    def _paint_polygon(self, painter, panel, cx, cy):
        b_c = QColor(panel.btn_border.text())
        f_c = QColor(panel.btn_fill.text())
        bw = panel.spn_bw.value()
        f_c.setAlpha(int(panel.sld_op.value() * 2.55))
        painter.setPen(QPen(b_c, bw))
        painter.setBrush(QBrush(f_c))
        dx, dy = 80, 55
        pts = [
            QPoint(cx, cy - dy), QPoint(cx + dx, cy),
            QPoint(cx, cy + dy), QPoint(cx - dx, cy)]
        painter.drawPolygon(QPolygon(pts))

    def _paint_line(self, painter, panel, w, h):
        clr = QColor(panel.btn_line_clr.text())
        lw = panel.spn_line_w.value()
        painter.setPen(QPen(clr, lw))
        path = QPainterPath()
        path.moveTo(w * 0.1, h * 0.7)
        path.cubicTo(w * 0.3, h * 0.2,
                     w * 0.6, h * 0.8,
                     w * 0.9, h * 0.3)
        painter.drawPath(path)

    def _paint_point(self, painter, panel, cx, cy):
        clr = QColor(panel.btn_icon_clr.text())
        scale = panel.spn_icon_scale.value()
        r = int(8 * scale)
        painter.setPen(QPen(clr.darker(120), 2))
        painter.setBrush(QBrush(clr))
        for dx, dy in [(0, 0), (-40, -25), (35, -20),
                       (-25, 30), (45, 25)]:
            painter.drawEllipse(
                QPoint(cx + dx, cy + dy), r, r)

    # ── Public ────────────────────────────────────────

    def refresh_geom_tabs(self):
        self.ensure_initialized()
        self._refresh_fields()

    def update_labels(self, lang):
        self.lang = lang
        if not self._initialized:
            return
        is_vi = lang == 'vi'
        self.btn_add.setText(
            "➕ Thêm SHP" if is_vi else "➕ Add SHP")
        self.btn_add_canvas.setText(
            "🗺 Lấy từ QGIS" if is_vi
            else "🗺 From QGIS")
        self.btn_remove.setText(
            "➖ Xóa" if is_vi else "➖ Remove")
        self.btn_clear.setText(
            "🗑 Xóa hết" if is_vi else "🗑 Clear")
        self.btn_export.setText(
            "🚀 XUẤT KML / KMZ" if is_vi
            else "🚀 EXPORT KML / KMZ")
        if hasattr(self, 'btn_save_cfg'):
            self.btn_save_cfg.setText(
                '💾 Lưu cấu hình' if is_vi
                else '💾 Save Config')
            self.btn_load_cfg.setText(
                '📂 Mở cấu hình' if is_vi
                else '📂 Load Config')
            self.btn_reset_cfg.setText('🔄 Reset')
        for gp in self.findChildren(QGroupBox):
            t = gp.title()
            if 'SHP' in t or 'file' in t.lower():
                gp.setTitle(
                    "1. Chọn file SHP" if is_vi
                    else "1. Select SHP Files")
            elif 'loại' in t or 'geometry' in t.lower():
                gp.setTitle(
                    "2. Thiết lập theo loại đối tượng"
                    if is_vi
                    else "2. Config by Geometry Type")
        self.panel_point.update_labels(lang)
        self.panel_line.update_labels(lang)
        self.panel_poly.update_labels(lang)

    # ── Private ───────────────────────────────────────

    def _on_file_clicked(self, index):
        row = index.row()
        if row < 0 or row >= len(self._shp_infos):
            return
        _, gk = self._shp_infos[row]
        tab_map = {'point': 0, 'line': 1, 'polygon': 2}
        self.geom_tabs.setCurrentIndex(
            tab_map.get(gk, 0))

    def _add_shp(self):
        paths, _ = QFileDialog.getOpenFileNames(
            self,
            "Chọn file SHP" if self.lang == 'vi'
            else "Select SHP",
            "", "Shapefile (*.shp)")
        if not paths:
            return
        for p in paths:
            if p in self._shp_paths:
                continue
            bn = os.path.splitext(
                os.path.basename(p))[0]
            layer = QgsVectorLayer(p, bn, 'ogr')
            if not layer.isValid():
                continue
            gt = layer.geometryType()
            info = self.GEOM_LABELS.get(gt)
            if info is None:
                continue
            gk, glabel = info
            self._shp_paths.append(p)
            self._shp_infos.append((bn, gk))
            row = self.tbl_files.rowCount()
            self.tbl_files.insertRow(row)
            # Col 0: checkbox
            chk = QCheckBox()
            chk.setChecked(True)
            chk.setToolTip('Chọn lớp này để xuất')
            chk.stateChanged.connect(self._update_summary)
            self.tbl_files.setCellWidget(row, 0, chk)
            # Col 1: name
            item_name = QTableWidgetItem(bn)
            item_name.setFlags(
                item_name.flags() & ~QtCompat.ItemIsEditable)
            self.tbl_files.setItem(row, 1, item_name)
            # Col 2: geometry
            item_geom = QTableWidgetItem(glabel)
            item_geom.setFlags(
                item_geom.flags() & ~QtCompat.ItemIsEditable)
            self.tbl_files.setItem(row, 2, item_geom)
            # Col 3: path
            item_path = QTableWidgetItem(p)
            item_path.setFlags(
                item_path.flags() & ~QtCompat.ItemIsEditable)
            self.tbl_files.setItem(row, 3, item_path)
        self._update_summary()
        self._refresh_fields()

    def _add_from_canvas(self):
        added = 0
        for layer in (
                QgsProject.instance().mapLayers().values()):
            if not isinstance(layer, QgsVectorLayer):
                continue
            src = layer.source()
            if not src:
                continue
            base_src = (src.split('|')[0]
                        if '|' in src else src)
            if not os.path.exists(base_src):
                continue
            if src in self._shp_paths:
                continue
            gt = layer.geometryType()
            info = self.GEOM_LABELS.get(gt)
            if info is None:
                continue
            gk, glabel = info
            bn = layer.name()
            self._shp_paths.append(src)
            self._shp_infos.append((bn, gk))
            row = self.tbl_files.rowCount()
            self.tbl_files.insertRow(row)
            # Col 0: checkbox
            chk = QCheckBox()
            chk.setChecked(True)
            chk.setToolTip('Chọn lớp này để xuất')
            chk.stateChanged.connect(self._update_summary)
            self.tbl_files.setCellWidget(row, 0, chk)
            # Col 1: name
            item_name = QTableWidgetItem(bn)
            item_name.setFlags(
                item_name.flags() & ~QtCompat.ItemIsEditable)
            self.tbl_files.setItem(row, 1, item_name)
            # Col 2: geometry
            item_geom = QTableWidgetItem(glabel)
            item_geom.setFlags(
                item_geom.flags() & ~QtCompat.ItemIsEditable)
            self.tbl_files.setItem(row, 2, item_geom)
            # Col 3: path (basename shown, full path as tooltip)
            item_path = QTableWidgetItem(
                os.path.basename(base_src))
            item_path.setFlags(
                item_path.flags() & ~QtCompat.ItemIsEditable)
            item_path.setToolTip(src)
            self.tbl_files.setItem(row, 3, item_path)
            added += 1
        if added == 0:
            QMessageBox.information(
                self, "Info",
                "Không tìm thấy layer nào!"
                if self.lang == 'vi'
                else "No layers found!")
        else:
            self._update_summary()
        self._refresh_fields()

    def _remove_shp(self):
        row = self.tbl_files.currentRow()
        if row < 0:
            return
        self._shp_paths.pop(row)
        self._shp_infos.pop(row)
        self.tbl_files.removeRow(row)
        self._update_summary()
        self._refresh_fields()

    def _clear_shp(self):
        self._shp_paths.clear()
        self._shp_infos.clear()
        self.tbl_files.setRowCount(0)
        self._update_summary()
        self._refresh_fields()

    def _refresh_fields(self):
        # Block signals during batch update
        self.geom_tabs.blockSignals(True)
        try:
            pt_layers, ln_layers, pg_layers = [], [], []
            for path, (bn, gk) in zip(
                    self._shp_paths, self._shp_infos):
                layer = QgsVectorLayer(path, 'tmp', 'ogr')
                if not layer.isValid():
                    continue
                fnames = sorted(
                    f.name() for f in layer.fields())
                # Find the "best" sample: feature with most non-null values
                # so label preview shows real data instead of "(không hiển thị)"
                sample = {}
                best_score = -1
                for feat in layer.getFeatures():
                    candidate = {
                        f.name(): feat[f.name()]
                        for f in feat.fields()
                    }
                    score = sum(
                        1 for v in candidate.values()
                        if v is not None
                        and str(v) not in ('NULL', 'None', 'null', '')  # noqa: W503
                    )
                    if score > best_score:
                        best_score = score
                        sample = candidate
                    if score == len(fnames):  # perfect: all fields non-null
                        break
                info = (path, bn, fnames, sample)
                if gk == 'point':
                    pt_layers.append(info)
                elif gk == 'line':
                    ln_layers.append(info)
                else:
                    pg_layers.append(info)
            self.panel_point.set_layers(pt_layers)
            self.panel_line.set_layers(ln_layers)
            self.panel_poly.set_layers(pg_layers)
        finally:
            self.geom_tabs.blockSignals(False)

    def _update_summary(self):
        n = len(self._shp_paths)
        n_sel = sum(
            1 for i in range(self.tbl_files.rowCount())
            if self.tbl_files.cellWidget(i, 0)
            and self.tbl_files.cellWidget(i, 0).isChecked())  # noqa: W503
        pts = sum(1 for _, g in self._shp_infos
                  if g == 'point')
        lns = sum(1 for _, g in self._shp_infos
                  if g == 'line')
        pgs = sum(1 for _, g in self._shp_infos
                  if g == 'polygon')
        self.lbl_summary.setText(
            f"📂 {n} files  ({n_sel} được chọn xuất)  "
            f"|  🔵 {pts} Point  "
            f"🟢 {lns} Line  🟡 {pgs} Polygon")

    def _export(self):
        # Collect only checked (selected) paths
        selected_paths = [
            self._shp_paths[i]
            for i in range(self.tbl_files.rowCount())
            if self.tbl_files.cellWidget(i, 0)
            and self.tbl_files.cellWidget(i, 0).isChecked()  # noqa: W503
        ]
        if not selected_paths:
            QMessageBox.warning(
                self,
                "Lỗi" if self.lang == 'vi' else "Error",
                "Không có layer nào được chọn!"
                if self.lang == 'vi'
                else "No layers selected!")
            return
        out_path, _ = QFileDialog.getSaveFileName(
            self,
            "Lưu KML / KMZ" if self.lang == 'vi'
            else "Save KML / KMZ",
            "", "KML (*.kml);;KMZ (*.kmz)")
        if not out_path:
            return

        panels = {
            'point': self.panel_point,
            'line': self.panel_line,
            'polygon': self.panel_poly,
        }
        path_cfg = {}
        for gk, panel in panels.items():
            for key, cfg in panel.get_all_layer_configs():
                path_cfg[key] = cfg

        layer_configs = []
        for path in selected_paths:
            cfg = path_cfg.get(path)
            if cfg:
                layer_configs.append((path, cfg))

        if not layer_configs:
            QMessageBox.warning(
                self,
                "Lỗi" if self.lang == 'vi' else "Error",
                "Không có layer nào để xuất!")
            return

        fmt = ('kmz' if out_path.lower().endswith('.kmz')
               else 'kml')
        scale_denominator = self._scale_values[self.cmb_scale.currentIndex()]
        builder = MergeKmlBuilder()
        ok, msg = builder.build(
            layer_configs, out_path, fmt,
            scale_denominator=scale_denominator)

        if ok:
            title = ("Thành công" if self.lang == 'vi'
                     else "Success")
            QMessageBox.information(self, title, msg)
        else:
            title = ("Lỗi" if self.lang == 'vi'
                     else "Error")
            QMessageBox.warning(self, title, msg)

    def _save_config(self):
        """Save current configuration to JSON file."""
        path, _ = QFileDialog.getSaveFileName(
            self, 'Save Config', '', 'JSON (*.json)')
        if not path:
            return
        try:
            configs = {}
            for panel_name, panel in [('point', self.panel_point),
                                      ('line', self.panel_line),
                                      ('polygon', self.panel_poly)]:
                all_cfgs = panel.get_all_layer_configs()
                if all_cfgs:
                    configs[panel_name] = all_cfgs
            with open(path, 'w', encoding='utf-8') as f:
                json.dump(configs, f, ensure_ascii=False, indent=2)
            QMessageBox.information(
                self, 'LVT4U',
                'Đã lưu cấu hình!' if self.lang == 'vi'
                else 'Config saved!')
        except Exception as e:
            QMessageBox.warning(self, 'LVT4U', str(e))

    def _load_config(self):
        """Load configuration from JSON file."""
        path, _ = QFileDialog.getOpenFileName(
            self, 'Load Config', '', 'JSON (*.json)')
        if not path:
            return
        try:
            with open(path, 'r', encoding='utf-8') as f:
                configs = json.load(f)

            self.panel_point.load_all_layer_configs(configs.get('point', []))
            self.panel_line.load_all_layer_configs(configs.get('line', []))
            self.panel_poly.load_all_layer_configs(configs.get('polygon', []))

            # Apply to panels - currently just notifies user
            QMessageBox.information(
                self, 'LVT4U',
                'Đã tải cấu hình!' if self.lang == 'vi'
                else 'Config loaded!')
        except Exception as e:
            QMessageBox.warning(self, 'LVT4U', str(e))

    def _reset_config(self):
        """Reset all panels to default configuration."""
        reply = QMessageBox.question(
            self, 'LVT4U',
            'Reset tất cả cấu hình về mặc định?' if self.lang == 'vi'
            else 'Reset all config to defaults?',
            MsgButton.Yes | MsgButton.No, MsgButton.No)
        if reply == MsgButton.Yes:
            self._refresh_fields()
            QMessageBox.information(
                self, 'LVT4U',
                'Đã reset!' if self.lang == 'vi' else 'Reset done!')
