# -*- coding: utf-8 -*-
"""FSC Harvest Map — Dialog (Dynamic Field Mapping).

All field references are user-selectable — works with ANY data structure.

Author: Lộc Vũ Trung (LVT) / Slow Forest
License: GPL-3.0
"""

import os
import subprocess

from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtWidgets import (
    QDialog, QVBoxLayout, QHBoxLayout, QGridLayout,
    QLabel, QPushButton, QComboBox, QLineEdit, QCheckBox,
    QGroupBox, QTableWidget, QTableWidgetItem, QHeaderView,
    QFileDialog, QProgressBar, QRadioButton,
    QAbstractItemView, QMessageBox, QSpinBox, QApplication,
    QScrollArea, QWidget, QSplitter,
)
from qgis.core import QgsProject, QgsVectorLayer

from ..shared.i18n import tr

_DEFAULT_TITLE = "BẢN ĐỒ TRÍCH LỤC LÔ RỪNG TRỒNG"
_DEFAULT_SUBTITLE = "Quản lý rừng bền vững và Chứng chỉ rừng FSC"
_DEFAULT_ORG = "Tên nhóm chứng chỉ rừng"
_NONE = "— (none) —"
_ALL = "— all —"
_GREEN = "#1B5E20"
_GREEN_MID = "#4CAF50"
_BTN = (
    "QPushButton{background:#1B5E20;color:white;border-radius:4px;"
    "padding:8px 16px;font-weight:bold;font-size:13px}"
    "QPushButton:hover{background:#2E7D32}"
    "QPushButton:disabled{background:#9E9E9E}")
_HELP = (
    "QPushButton{background:#E8F5E9;border:1px solid #4CAF50;"
    "border-radius:4px;padding:4px 14px;font-weight:bold}"
    "QPushButton:hover{background:#C8E6C9}")

# Known field name hints for auto-detection
_HINTS = {
    "owner": ["Churung", "ChuRung", "Owner", "Ten_CR", "churung"],
    "plotid": ["PlotID_New", "Plot_ID", "PlotID", "Ma_Lo", "MaLo"],
    "area": ["Area", "DienTich", "Dien_Tich", "area", "AREA", "Shape_Area"],
    "commune": ["Xa", "xa", "Commune", "commune", "Ward", "Ten_Xa"],
    "month": ["ThangKT", "Thang_KT", "Month", "thang", "HarvestMonth"],
    "year": ["FSC", "Nam", "Year", "NamTrong", "year", "Planted"],
}


class FscHarvestDialog(QDialog):
    """FSC Harvest Map — fully dynamic field mapping."""

    def __init__(self, iface, plugin_dir, parent=None):
        super().__init__(parent)
        self.iface = iface
        self.plugin_dir = plugin_dir
        self._features = []

        self.setWindowTitle("LVT4U — " + tr("FSC Plot Layout"))
        self.setWindowFlags(
            self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
        self.setMinimumSize(860, 780)
        screen = QApplication.primaryScreen()
        if screen:
            sg = screen.availableGeometry()
            self.resize(int(sg.width() * 0.52),
                        int(sg.height() * 0.95))

        self._build_ui()
        self._connect_signals()
        self._refresh_layers()
        self._load_settings()

    # ================================================================
    # UI
    # ================================================================

    def _build_ui(self):
        outer = QVBoxLayout(self)
        outer.setContentsMargins(4, 4, 4, 4)
        outer.setSpacing(4)
        _gs = f"QGroupBox{{font-weight:bold;color:{_GREEN}}}"

        # ── Splitter: top (data+plots) / bottom (settings+output) ──
        splitter = QSplitter(Qt.Vertical)

        # --- TOP panel: ① + ② ---
        top_w = QWidget()
        root = QVBoxLayout(top_w)
        root.setSpacing(4)
        root.setContentsMargins(0, 0, 0, 0)

        # ── ① Data Selection ──
        g1 = QGroupBox("① " + tr("Data Selection"))
        g1.setStyleSheet(_gs)
        lay1 = QGridLayout(g1)
        lay1.setSpacing(4)

        lay1.addWidget(QLabel(tr("Main layer") + ":"), 0, 0)
        self.cmb_layer = QComboBox()
        lay1.addWidget(self.cmb_layer, 0, 1, 1, 5)

        # Dynamic field mapping — 6 selectors
        self._field_combos = {}
        field_defs = [
            ("owner", tr("Owner field"), 1, 0),
            ("plotid", tr("Plot ID field"), 1, 2),
            ("area", tr("Area field"), 1, 4),
            ("commune", tr("Commune field"), 2, 0),
            ("month", tr("Month field"), 2, 2),
            ("year", tr("Year field"), 2, 4),
        ]
        for key, label, row, col in field_defs:
            lay1.addWidget(QLabel(label + ":"), row, col)
            cmb = QComboBox()
            cmb.addItem(_NONE)
            self._field_combos[key] = cmb
            lay1.addWidget(cmb, row, col + 1)

        # Auxiliary layers — compact 2-column grid
        lay1.addWidget(QLabel(tr("Auxiliary layers") + ":"), 3, 0)
        aux = QGridLayout()
        aux.setSpacing(2)
        self._aux_checks = []
        self._aux_combos = []
        self._aux_names = []
        _aux_defs = [
            (tr("Contour lines"), "contour"),
            (tr("Sub-compartment boundary"), "boundary"),
            (tr("Roads"), "roads"),
            (tr("Compartment labels"), "labels"),
        ]
        for i, (label, key) in enumerate(_aux_defs):
            chk = QCheckBox(label)
            cmb = QComboBox()
            setattr(self, f"chk_{key}", chk)
            setattr(self, f"cmb_{key}", cmb)
            self._aux_checks.append(chk)
            self._aux_combos.append(cmb)
            row, col = divmod(i, 2)
            aux.addWidget(chk, row, col * 2)
            aux.addWidget(cmb, row, col * 2 + 1)
        lay1.addLayout(aux, 3, 1, 1, 4)

        root.addWidget(g1)

        # ── Toolbar row: Save / Load / Load Data / Deselect / Select All ──
        sel = QHBoxLayout()
        self.btn_save_cfg = QPushButton("💾 " + tr("Save"))
        self.btn_save_cfg.setFixedHeight(26)
        self.btn_save_cfg.setStyleSheet(_HELP)
        self.btn_save_cfg.setToolTip(tr("Save current settings for next session"))
        sel.addWidget(self.btn_save_cfg)
        self.btn_load_cfg = QPushButton("📂 " + tr("Load"))
        self.btn_load_cfg.setFixedHeight(26)
        self.btn_load_cfg.setStyleSheet(_HELP)
        self.btn_load_cfg.setToolTip(tr("Reload saved settings"))
        sel.addWidget(self.btn_load_cfg)
        self.btn_load = QPushButton("🔄 " + tr("Load data"))
        self.btn_load.setFixedHeight(26)
        self.btn_load.setStyleSheet(_HELP)
        sel.addWidget(self.btn_load)
        self.btn_desel = QPushButton("☐ " + tr("Deselect All"))
        self.btn_desel.setFixedHeight(26)
        sel.addWidget(self.btn_desel)
        self.btn_sel_all = QPushButton("☑ " + tr("Select All"))
        self.btn_sel_all.setFixedHeight(26)
        sel.addWidget(self.btn_sel_all)
        sel.addStretch()
        self.lbl_count = QLabel(tr("Selected") + ": 0 / 0")
        self.lbl_count.setStyleSheet("font-weight:bold")
        sel.addWidget(self.lbl_count)
        root.addLayout(sel)

        # ── ② Plot Selection ──
        g2 = QGroupBox("② " + tr("Plot Selection"))
        g2.setStyleSheet(_gs)
        v2 = QVBoxLayout(g2)

        # Dynamic filters — 3 combo pairs (field selector + value)
        filt_row = QHBoxLayout()
        filt_row.addWidget(QLabel(tr("Filter") + ":"))
        self._filter_fields = []
        self._filter_values = []
        for i in range(3):
            cf = QComboBox()
            cf.setMinimumWidth(90)
            cf.addItem(_NONE)
            cv = QComboBox()
            cv.setMinimumWidth(90)
            cv.addItem(_ALL)
            self._filter_fields.append(cf)
            self._filter_values.append(cv)
            filt_row.addWidget(cf)
            filt_row.addWidget(cv)
        self.btn_filter = QPushButton("🔍")
        self.btn_filter.setFixedWidth(36)
        filt_row.addWidget(self.btn_filter)
        v2.addLayout(filt_row)

        # Table — dynamic columns
        self.tbl_plots = QTableWidget()
        self.tbl_plots.setColumnCount(7)
        self.tbl_plots.setHorizontalHeaderLabels([
            "✓", tr("Owner"), tr("Plot ID"), tr("Area"),
            tr("Commune"), tr("Month"), tr("Year"),
        ])
        hdr = self.tbl_plots.horizontalHeader()
        hdr.setSectionResizeMode(0, QHeaderView.Fixed)
        hdr.resizeSection(0, 30)
        hdr.setSectionResizeMode(1, QHeaderView.Stretch)
        self.tbl_plots.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.tbl_plots.setAlternatingRowColors(True)
        self.tbl_plots.verticalHeader().setDefaultSectionSize(24)
        self.tbl_plots.setMinimumHeight(120)
        v2.addWidget(self.tbl_plots, 1)  # stretch=1
        root.addWidget(g2, 1)  # ② gets stretch
        splitter.addWidget(top_w)

        # --- BOTTOM panel: ③ + ④ (scrollable) ---
        bot_scroll = QScrollArea()
        bot_scroll.setWidgetResizable(True)
        bot_scroll.setFrameShape(QScrollArea.NoFrame)
        bot_w = QWidget()
        bot_lay = QVBoxLayout(bot_w)
        bot_lay.setSpacing(4)
        bot_lay.setContentsMargins(0, 0, 0, 0)
        bot_scroll.setWidget(bot_w)

        # ── ③ Map Settings ──
        g3 = QGroupBox("③ " + tr("Map Settings"))
        g3.setStyleSheet(_gs)
        lay3 = QGridLayout(g3)
        lay3.setSpacing(4)
        lay3.addWidget(QLabel(tr("Title") + ":"), 0, 0)
        self.txt_title = QLineEdit(_DEFAULT_TITLE)
        lay3.addWidget(self.txt_title, 0, 1, 1, 5)
        lay3.addWidget(QLabel(tr("Subtitle") + ":"), 1, 0)
        self.txt_subtitle = QLineEdit(_DEFAULT_SUBTITLE)
        lay3.addWidget(self.txt_subtitle, 1, 1, 1, 5)
        lay3.addWidget(QLabel(tr("Organization") + ":"), 2, 0)
        self.txt_org = QLineEdit(_DEFAULT_ORG)
        lay3.addWidget(self.txt_org, 2, 1, 1, 5)
        lay3.addWidget(QLabel(tr("Scale") + ":"), 3, 0)
        self.rad_auto = QRadioButton("Auto (1:1000→1:25000)")
        self.rad_auto.setChecked(True)
        lay3.addWidget(self.rad_auto, 3, 1)
        self.rad_custom = QRadioButton(tr("Custom") + ":")
        lay3.addWidget(self.rad_custom, 3, 2)
        self.spn_scale = QSpinBox()
        self.spn_scale.setRange(500, 50000)
        self.spn_scale.setValue(2000)
        self.spn_scale.setPrefix("1:")
        self.spn_scale.setEnabled(False)
        lay3.addWidget(self.spn_scale, 3, 3)

        # Plot label — fraction style (numerator / denominator)
        lay3.addWidget(QLabel(tr("Plot label") + ":"), 4, 0)
        self.chk_show_label = QCheckBox(tr("Show label"))
        self.chk_show_label.setChecked(True)
        lay3.addWidget(self.chk_show_label, 4, 1)
        lbl_line = QHBoxLayout()
        lbl_line.addWidget(QLabel(tr("Line length") + ":"))
        self.spn_line_len = QSpinBox()
        self.spn_line_len.setRange(3, 20)
        self.spn_line_len.setValue(6)
        self.spn_line_len.setToolTip(tr("Number of '─' characters in separator"))
        lbl_line.addWidget(self.spn_line_len)
        lay3.addLayout(lbl_line, 4, 2, 1, 2)
        # Numerator fields
        lay3.addWidget(QLabel("  " + tr("Numerator") + ":"), 5, 0)
        self.cmb_num1 = QComboBox()
        self.cmb_num1.addItem(_NONE)
        self.cmb_num2 = QComboBox()
        self.cmb_num2.addItem(_NONE)
        self.cmb_num3 = QComboBox()
        self.cmb_num3.addItem(_NONE)
        self.edt_num_sep = QLineEdit("-")
        self.edt_num_sep.setFixedWidth(30)
        num_row = QHBoxLayout()
        num_row.addWidget(self.cmb_num1)
        num_row.addWidget(self.edt_num_sep)
        num_row.addWidget(self.cmb_num2)
        num_row.addWidget(self.cmb_num3)
        lay3.addLayout(num_row, 5, 1, 1, 5)
        # Denominator fields
        lay3.addWidget(QLabel("  " + tr("Denominator") + ":"), 6, 0)
        self.cmb_den1 = QComboBox()
        self.cmb_den1.addItem(_NONE)
        self.cmb_den2 = QComboBox()
        self.cmb_den2.addItem(_NONE)
        self.cmb_den3 = QComboBox()
        self.cmb_den3.addItem(_NONE)
        self.edt_den_sep = QLineEdit("-")
        self.edt_den_sep.setFixedWidth(30)
        den_row = QHBoxLayout()
        den_row.addWidget(self.cmb_den1)
        den_row.addWidget(self.edt_den_sep)
        den_row.addWidget(self.cmb_den2)
        den_row.addWidget(self.cmb_den3)
        lay3.addLayout(den_row, 6, 1, 1, 5)

        # Store label combos for easy iteration
        self._num_combos = [self.cmb_num1, self.cmb_num2, self.cmb_num3]
        self._den_combos = [self.cmb_den1, self.cmb_den2, self.cmb_den3]

        # --- Right column: Signatures + Output ---
        right_col = QVBoxLayout()
        right_col.setSpacing(4)

        # Ô ký section
        g_sig = QGroupBox(tr("Signatures & Legend"))
        g_sig.setStyleSheet(_gs)
        sig_lay = QGridLayout(g_sig)
        sig_lay.setSpacing(4)
        sig_lay.addWidget(QLabel(tr("Signatures") + ":"), 0, 0)
        self.chk_sig_owner = QCheckBox(tr("Plot owner"))
        self.chk_sig_owner.setChecked(True)
        sig_lay.addWidget(self.chk_sig_owner, 0, 1)
        self.chk_sig_group = QCheckBox(tr("Group representative"))
        self.chk_sig_group.setChecked(True)
        sig_lay.addWidget(self.chk_sig_group, 0, 2)
        self.chk_legend = QCheckBox(tr("Show legend"))
        self.chk_legend.setChecked(True)
        sig_lay.addWidget(self.chk_legend, 1, 1)
        # Group rep name: field or manual
        sig_lay.addWidget(QLabel(tr("Group rep") + ":"), 2, 0)
        grp_row = QHBoxLayout()
        self.cmb_group_name = QComboBox()
        self.cmb_group_name.addItem(_NONE)
        self.cmb_group_name.setToolTip(
            tr("Select field for group rep name, or type below"))
        grp_row.addWidget(self.cmb_group_name)
        grp_row.addWidget(QLabel(tr("or") + ":"))
        self.txt_group_name = QLineEdit()
        self.txt_group_name.setPlaceholderText(
            tr("Type group rep name manually"))
        grp_row.addWidget(self.txt_group_name)
        sig_lay.addLayout(grp_row, 2, 1, 1, 2)
        right_col.addWidget(g_sig)

        # ── ④ Output ──
        g4 = QGroupBox("④ " + tr("Output"))
        g4.setStyleSheet(_gs)
        lay4 = QGridLayout(g4)
        lay4.addWidget(QLabel("📂 " + tr("Output folder") + ":"), 0, 0)
        self.txt_output = QLineEdit()
        lay4.addWidget(self.txt_output, 0, 1)
        self.btn_browse = QPushButton("...")
        self.btn_browse.setFixedWidth(36)
        lay4.addWidget(self.btn_browse, 0, 2)
        self.rad_separate = QRadioButton(
            tr("Separate PDFs") + " (1 file / plot)")
        self.rad_separate.setChecked(True)
        lay4.addWidget(self.rad_separate, 1, 0, 1, 2)
        self.rad_merged = QRadioButton(
            tr("Merged PDF") + " (1 file, N pages)")
        lay4.addWidget(self.rad_merged, 1, 2)
        self.chk_open_after = QCheckBox(tr("Open output after export"))
        self.chk_open_after.setChecked(True)
        lay4.addWidget(self.chk_open_after, 2, 0, 1, 3)
        right_col.addWidget(g4)
        right_col.addStretch()

        # --- Assemble 2-column bottom ---
        two_col = QHBoxLayout()
        two_col.setSpacing(6)
        two_col.addWidget(g3, 3)  # left column wider
        two_col.addLayout(right_col, 2)  # right column
        bot_lay.addLayout(two_col)
        splitter.addWidget(bot_scroll)

        # Splitter proportions: top=45%, bottom=55%
        splitter.setStretchFactor(0, 9)
        splitter.setStretchFactor(1, 11)
        outer.addWidget(splitter, 1)

        # Progress (fixed bottom)
        self.progress = QProgressBar()
        self.progress.setVisible(False)
        self.progress.setStyleSheet(
            f"QProgressBar::chunk{{background:{_GREEN_MID}}}")
        outer.addWidget(self.progress)
        self.lbl_status = QLabel("")
        self.lbl_status.setStyleSheet("color:#666;font-style:italic")
        outer.addWidget(self.lbl_status)

        # Buttons (fixed bottom — only action buttons)
        bb = QHBoxLayout()
        self.btn_help = QPushButton("❓ " + tr("Help"))
        self.btn_help.setStyleSheet(_HELP)
        self.btn_help.setMinimumHeight(32)
        bb.addWidget(self.btn_help)
        bb.addStretch()
        self.btn_preview = QPushButton("👁 " + tr("Preview"))
        self.btn_preview.setStyleSheet(
            "QPushButton{background:#E3F2FD;border:1px solid #1976D2;"
            "border-radius:4px;padding:8px 16px;font-weight:bold;"
            "font-size:13px;color:#1565C0}"
            "QPushButton:hover{background:#BBDEFB}"
            "QPushButton:disabled{background:#9E9E9E;color:#666}")
        self.btn_preview.setMinimumHeight(40)
        self.btn_preview.setToolTip(
            tr("Open Layout Designer to preview the first selected plot"))
        bb.addWidget(self.btn_preview)
        self.btn_export = QPushButton("🖨️ " + tr("Export PDF"))
        self.btn_export.setStyleSheet(_BTN)
        self.btn_export.setMinimumHeight(40)
        self.btn_export.setMinimumWidth(180)
        bb.addWidget(self.btn_export)
        self.btn_close = QPushButton(tr("Close"))
        self.btn_close.setMinimumHeight(36)
        bb.addWidget(self.btn_close)
        outer.addLayout(bb)

    # ================================================================
    # Signals
    # ================================================================

    def _connect_signals(self):
        self.cmb_layer.currentIndexChanged.connect(self._on_layer_changed)
        self.btn_load.clicked.connect(self._load_features)
        self.btn_filter.clicked.connect(self._apply_filter)
        self.btn_sel_all.clicked.connect(self._select_all)
        self.btn_desel.clicked.connect(self._deselect_all)
        self.btn_browse.clicked.connect(self._browse_output)
        self.btn_preview.clicked.connect(self._do_preview)
        self.btn_export.clicked.connect(self._do_export)
        self.btn_help.clicked.connect(self._show_help)
        self.btn_save_cfg.clicked.connect(self._on_save_settings)
        self.btn_load_cfg.clicked.connect(self._on_load_settings)
        self.btn_close.clicked.connect(self.close)
        self.rad_custom.toggled.connect(
            lambda c: self.spn_scale.setEnabled(c))
        self.tbl_plots.cellChanged.connect(self._update_count)
        # When filter field changes → repopulate filter values
        for i, cf in enumerate(self._filter_fields):
            cf.currentIndexChanged.connect(
                lambda idx, fi=i: self._on_filter_field_changed(fi))

    # ================================================================
    # Layer / Field Management
    # ================================================================

    def _refresh_layers(self):
        """Populate layer combos from project."""
        project = QgsProject.instance()
        vectors = [lyr for lyr in project.mapLayers().values()
                   if isinstance(lyr, QgsVectorLayer)]
        polygons = [lyr for lyr in vectors if lyr.geometryType() == 2]

        self.cmb_layer.blockSignals(True)
        self.cmb_layer.clear()
        for lyr in polygons:
            self.cmb_layer.addItem(lyr.name(), lyr.id())
        self.cmb_layer.blockSignals(False)

        for cmb in [self.cmb_contour, self.cmb_boundary,
                    self.cmb_roads, self.cmb_labels]:
            cmb.clear()
            cmb.addItem("—", "")
            for lyr in vectors:
                cmb.addItem(lyr.name(), lyr.id())

        if self.cmb_layer.count():
            self._on_layer_changed(0)

    def _on_layer_changed(self, idx):
        """Layer changed → populate all field combos."""
        layer = self._get_main_layer()
        if not layer:
            return
        field_names = [f.name() for f in layer.fields()]

        # Populate 6 field mapping combos
        for key, cmb in self._field_combos.items():
            cmb.blockSignals(True)
            cmb.clear()
            cmb.addItem(_NONE)
            for fn in field_names:
                cmb.addItem(fn)
            # Auto-detect by hint
            hints = _HINTS.get(key, [])
            for hint in hints:
                if hint in field_names:
                    cmb.setCurrentText(hint)
                    break
            cmb.blockSignals(False)

        # Populate label combos (numerator/denominator)
        for cmb in self._num_combos + self._den_combos:
            cmb.blockSignals(True)
            cmb.clear()
            cmb.addItem(_NONE)
            for fn in field_names:
                cmb.addItem(fn)
            cmb.blockSignals(False)

        # Auto-set label defaults: num=PlotID, den=Area
        plotid_f = self._field_combos["plotid"].currentText()
        area_f = self._field_combos["area"].currentText()
        if plotid_f != _NONE:
            self.cmb_num1.setCurrentText(plotid_f)
        if area_f != _NONE:
            self.cmb_den1.setCurrentText(area_f)

        # Populate 3 filter field combos
        for cf in self._filter_fields:
            cf.blockSignals(True)
            cf.clear()
            cf.addItem(_NONE)
            for fn in field_names:
                cf.addItem(fn)
            cf.blockSignals(False)

        # Auto-set filter fields to commune/month/year
        auto_filter = ["commune", "month", "year"]
        for i, key in enumerate(auto_filter):
            mapped = self._field_combos[key].currentText()
            if mapped != _NONE:
                self._filter_fields[i].setCurrentText(mapped)

        # Populate group rep name combo
        self.cmb_group_name.blockSignals(True)
        self.cmb_group_name.clear()
        self.cmb_group_name.addItem(_NONE)
        for fn in field_names:
            self.cmb_group_name.addItem(fn)
        self.cmb_group_name.blockSignals(False)

        # Auto-load
        self._load_features()

    def _get_main_layer(self):
        idx = self.cmb_layer.currentIndex()
        if idx < 0:
            return None
        return QgsProject.instance().mapLayer(
            self.cmb_layer.itemData(idx))

    def _fval(self, feat, key):
        """Get feature value for a mapped field. Returns str or ''."""
        cmb = self._field_combos.get(key)
        if not cmb:
            return ""
        fn = cmb.currentText()
        if fn == _NONE:
            return ""
        names = feat.fields().names()
        if fn not in names:
            return ""
        v = feat[fn]
        if v is None or str(v) == "NULL":
            return ""
        return str(v)

    # ================================================================
    # Feature Loading
    # ================================================================

    def _load_features(self):
        """Load features and populate table + filters."""
        layer = self._get_main_layer()
        if not layer:
            return
        self._features = list(layer.getFeatures())

        # Populate filter value combos
        for i, cf in enumerate(self._filter_fields):
            self._populate_filter_values(i)

        self._populate_table(self._features)
        self.lbl_status.setText(
            f"✅ {len(self._features)} features loaded")

    def _populate_filter_values(self, idx):
        """Populate filter value combo for filter slot idx."""
        cv = self._filter_values[idx]
        cf = self._filter_fields[idx]
        fn = cf.currentText()

        cv.blockSignals(True)
        cv.clear()
        cv.addItem(_ALL)

        if fn != _NONE and self._features:
            vals = set()
            for feat in self._features:
                names = feat.fields().names()
                if fn in names:
                    v = feat[fn]
                    if v is not None and str(v) != "NULL":
                        vals.add(str(v))
            for v in sorted(vals, key=lambda x: (
                    not x.isdigit(), int(x) if x.isdigit() else x)):
                cv.addItem(v)
        cv.blockSignals(False)

    def _on_filter_field_changed(self, idx):
        """When a filter field selector changes, repopulate its values."""
        self._populate_filter_values(idx)

    # ================================================================
    # Table
    # ================================================================

    def _populate_table(self, features):
        self.tbl_plots.blockSignals(True)
        self.tbl_plots.setRowCount(0)

        for feat in features:
            row = self.tbl_plots.rowCount()
            self.tbl_plots.insertRow(row)

            # Col 0: checkbox
            chk = QTableWidgetItem()
            chk.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
            chk.setCheckState(Qt.Unchecked)
            chk.setData(Qt.UserRole, feat.id())
            self.tbl_plots.setItem(row, 0, chk)

            # Cols 1-6: dynamic fields
            keys = ["owner", "plotid", "area", "commune", "month", "year"]
            for col, key in enumerate(keys, 1):
                val = self._fval(feat, key)
                # Format area nicely
                if key == "area" and val:
                    try:
                        val = f"{float(val):.2f}"
                    except ValueError:
                        pass
                item = QTableWidgetItem(val)
                item.setFlags(item.flags() & ~Qt.ItemIsEditable)
                self.tbl_plots.setItem(row, col, item)

        self.tbl_plots.blockSignals(False)
        self._update_count()

    # ================================================================
    # Filters & Selection
    # ================================================================

    def _apply_filter(self):
        filtered = list(self._features)
        for i in range(3):
            fn = self._filter_fields[i].currentText()
            fv = self._filter_values[i].currentText()
            if fn == _NONE or fv == _ALL:
                continue
            filtered = [
                f for f in filtered
                if fn in f.fields().names()
                and str(f[fn] or "") == fv  # noqa: W503
            ]
        self._populate_table(filtered)

    def _select_all(self):
        self.tbl_plots.blockSignals(True)
        for r in range(self.tbl_plots.rowCount()):
            it = self.tbl_plots.item(r, 0)
            if it:
                it.setCheckState(Qt.Checked)
        self.tbl_plots.blockSignals(False)
        self._update_count()

    def _deselect_all(self):
        self.tbl_plots.blockSignals(True)
        for r in range(self.tbl_plots.rowCount()):
            it = self.tbl_plots.item(r, 0)
            if it:
                it.setCheckState(Qt.Unchecked)
        self.tbl_plots.blockSignals(False)
        self._update_count()

    def _update_count(self):
        total = self.tbl_plots.rowCount()
        sel = sum(1 for r in range(total)
                  if self.tbl_plots.item(r, 0)
                  and self.tbl_plots.item(r, 0).checkState() == Qt.Checked)  # noqa: W503
        self.lbl_count.setText(f"{tr('Selected')}: {sel}/{total}")

    # ================================================================
    # Output
    # ================================================================

    def _browse_output(self):
        path = QFileDialog.getExistingDirectory(
            self, tr("Select output folder"),
            self.txt_output.text() or "")
        if path:
            self.txt_output.setText(path)

    # ================================================================
    # Export
    # ================================================================

    def _do_export(self):
        layer = self._get_main_layer()
        if not layer:
            QMessageBox.warning(self, "LVT4U",
                                tr("Please select a main layer."))
            return
        output_dir = self.txt_output.text().strip()
        if not output_dir:
            QMessageBox.warning(self, "LVT4U",
                                tr("Please select an output folder."))
            return

        selected_ids = []
        for r in range(self.tbl_plots.rowCount()):
            it = self.tbl_plots.item(r, 0)
            if it and it.checkState() == Qt.Checked:
                selected_ids.append(it.data(Qt.UserRole))
        if not selected_ids:
            QMessageBox.warning(self, "LVT4U",
                                tr("Please select at least one plot."))
            return

        features = [f for f in self._features if f.id() in selected_ids]

        # Collect aux layers (only ticked ones)
        aux_layers = []
        for chk, cmb in [
            (self.chk_contour, self.cmb_contour),
            (self.chk_boundary, self.cmb_boundary),
            (self.chk_roads, self.cmb_roads),
            (self.chk_labels, self.cmb_labels),
        ]:
            if chk.isChecked() and cmb.currentData():
                lyr = QgsProject.instance().mapLayer(cmb.currentData())
                if lyr:
                    aux_layers.append(lyr)

        # Build label expression (fraction: numerator / denominator)
        label_expr = self._build_label_expression()

        # Build params with dynamic field names
        params = {
            "owner_field": self._field_combos["owner"].currentText(),
            "plot_id_field": self._field_combos["plotid"].currentText(),
            "area_field": self._field_combos["area"].currentText(),
            "month_field": self._field_combos["month"].currentText(),
            "year_field": self._field_combos["year"].currentText(),
            "commune_field": self._field_combos["commune"].currentText(),
            "title": self.txt_title.text(),
            "subtitle": self.txt_subtitle.text(),
            "org_name": self.txt_org.text(),
            "output_dir": output_dir,
            "merge_pdf": self.rad_merged.isChecked(),
            "sig_owner": self.chk_sig_owner.isChecked(),
            "sig_group": self.chk_sig_group.isChecked(),
            "group_rep_field": self.cmb_group_name.currentText(),
            "group_rep_name": self.txt_group_name.text().strip(),
            "show_legend": self.chk_legend.isChecked(),
            "show_label": self.chk_show_label.isChecked(),
            "label_expr": label_expr,
            "custom_scale": (self.spn_scale.value()
                             if self.rad_custom.isChecked() else None),
            "main_layer": layer,
            "aux_layers": aux_layers,
            "map_crs": QgsProject.instance().crs(),
            "none_marker": _NONE,
        }

        self.btn_export.setEnabled(False)
        self.progress.setVisible(True)
        self.progress.setMaximum(len(features))
        self.progress.setValue(0)

        def on_progress(current, total, msg):
            self.progress.setValue(current)
            self.lbl_status.setText(msg)
            QApplication.processEvents()

        try:
            from .exporter import export_plots
            pdf_paths = export_plots(
                self.iface, self.plugin_dir,
                features, params, on_progress)
            self.progress.setValue(len(features))
            self.lbl_status.setText(
                f"✅ {tr('Done')}! {len(pdf_paths)} PDF(s) → {output_dir}")
            QMessageBox.information(
                self, "LVT4U",
                f"{tr('Export complete')}!\n\n"
                f"{len(pdf_paths)} PDF(s) → {output_dir}")
            # Open output folder if checked
            if self.chk_open_after.isChecked() and pdf_paths:
                open_path = pdf_paths[0] if len(pdf_paths) == 1 else output_dir
                try:
                    os.startfile(open_path)
                except Exception:
                    subprocess.Popen(['explorer', output_dir])
        except Exception as e:
            QMessageBox.critical(self, "LVT4U",
                                 f"{tr('Export failed')}:\n{e}")
            self.lbl_status.setText(f"❌ {e}")
        finally:
            self.btn_export.setEnabled(True)

    # ================================================================
    # Help
    # ================================================================

    # ================================================================
    # Label Expression Builder
    # ================================================================

    def _build_label_expression(self):
        """Build QGIS fraction-style label expression from num / den combos.

        Features:
        - Line length controlled by spn_line_len spinbox
        - Auto-appends ' ha' if area field is detected in denominator
        """
        num_sep = self.edt_num_sep.text() or "-"
        den_sep = self.edt_den_sep.text() or "-"

        num_fields = [c.currentText() for c in self._num_combos
                      if c.currentText() != _NONE]
        den_fields = [c.currentText() for c in self._den_combos
                      if c.currentText() != _NONE]

        if not num_fields and not den_fields:
            return ""

        # Detect which field the user mapped as "area"
        area_field_name = self._field_combos["area"].currentText()

        def _expr(fields, sep, auto_ha=False):
            parts = []
            for f in fields:
                p = f"coalesce(\"{f}\", '')"
                # Auto-append ' ha' when field matches the area mapping
                if auto_ha and f == area_field_name and area_field_name != _NONE:
                    p = f"({p}) || ' ha'"
                parts.append(p)
            return (f" || '{sep}' || ".join(parts)) if parts else "''"

        num_expr = _expr(num_fields, num_sep)

        if not den_fields:
            return num_expr

        den_expr = _expr(den_fields, den_sep, auto_ha=True)
        line_len = self.spn_line_len.value()
        underline = "─" * line_len
        return (
            f"({num_expr}) || '\\n' || '{underline}' || "
            f"'\\n' || ({den_expr})"
        )

    def _show_help(self):
        from ..shared.help_dialog import HelpDialog
        dlg = HelpDialog("fsc_harvest", parent=self)
        dlg.exec_()

    # ================================================================
    # Preview
    # ================================================================

    def _do_preview(self):
        """Preview layout for the first selected plot in QGIS Designer."""
        layer = self._get_main_layer()
        if not layer:
            QMessageBox.warning(self, "LVT4U",
                                tr("Please select a main layer."))
            return

        # Get first selected feature
        selected_ids = []
        for r in range(self.tbl_plots.rowCount()):
            it = self.tbl_plots.item(r, 0)
            if it and it.checkState() == Qt.Checked:
                selected_ids.append(it.data(Qt.UserRole))
        if not selected_ids:
            QMessageBox.warning(self, "LVT4U",
                                tr("Please select at least one plot."))
            return

        feat = None
        for f in self._features:
            if f.id() == selected_ids[0]:
                feat = f
                break
        if not feat:
            return

        # Collect aux layers
        aux_layers = []
        for chk, cmb in [
            (self.chk_contour, self.cmb_contour),
            (self.chk_boundary, self.cmb_boundary),
            (self.chk_roads, self.cmb_roads),
            (self.chk_labels, self.cmb_labels),
        ]:
            if chk.isChecked() and cmb.currentData():
                lyr = QgsProject.instance().mapLayer(cmb.currentData())
                if lyr:
                    aux_layers.append(lyr)

        label_expr = self._build_label_expression()

        params = {
            "owner_field": self._field_combos["owner"].currentText(),
            "plot_id_field": self._field_combos["plotid"].currentText(),
            "area_field": self._field_combos["area"].currentText(),
            "month_field": self._field_combos["month"].currentText(),
            "year_field": self._field_combos["year"].currentText(),
            "commune_field": self._field_combos["commune"].currentText(),
            "title": self.txt_title.text(),
            "subtitle": self.txt_subtitle.text(),
            "org_name": self.txt_org.text(),
            "sig_owner": self.chk_sig_owner.isChecked(),
            "sig_group": self.chk_sig_group.isChecked(),
            "group_rep_field": self.cmb_group_name.currentText(),
            "group_rep_name": self.txt_group_name.text().strip(),
            "show_legend": self.chk_legend.isChecked(),
            "show_label": self.chk_show_label.isChecked(),
            "label_expr": label_expr,
            "custom_scale": (self.spn_scale.value()
                             if self.rad_custom.isChecked() else None),
            "main_layer": layer,
            "aux_layers": aux_layers,
            "map_crs": QgsProject.instance().crs(),
            "none_marker": _NONE,
        }

        self.btn_preview.setEnabled(False)
        self.btn_preview.setText("⏳...")
        QApplication.setOverrideCursor(Qt.WaitCursor)
        QApplication.processEvents()

        try:
            from .exporter import preview_plot
            layout = preview_plot(
                self.iface, self.plugin_dir, feat, params)
            if layout:
                designer = self.iface.openLayoutDesigner(layout)
                try:
                    designer.view().zoomFull()
                except Exception:
                    pass
                self.lbl_status.setText(
                    "✅ " + tr("Preview opened in Layout Designer"))
        except Exception as e:
            QMessageBox.critical(self, "LVT4U",
                                 f"{tr('Preview failed')}:\n{e}")
            self.lbl_status.setText(f"❌ {e}")
        finally:
            QApplication.restoreOverrideCursor()
            self.btn_preview.setEnabled(True)
            self.btn_preview.setText("👁 " + tr("Preview"))

    # ================================================================
    # Settings Persistence (JSON file)
    # ================================================================

    def _default_settings_dir(self):
        """Default directory for settings files."""
        return os.path.join(self.plugin_dir, "fsc_harvest")

    def _on_save_settings(self):
        """Button handler: Save As dialog for settings JSON."""
        default_dir = self._default_settings_dir()
        os.makedirs(default_dir, exist_ok=True)
        default_path = os.path.join(default_dir,
                                    "fsc_harvest_settings.json")
        path, _ = QFileDialog.getSaveFileName(
            self, tr("Save Settings"), default_path,
            "JSON Files (*.json);;All Files (*)")
        if not path:
            return
        self._save_settings(path)
        self.lbl_status.setText(
            "✅ " + tr("Settings saved") + f": {os.path.basename(path)}")

    def _on_load_settings(self):
        """Button handler: Open dialog for settings JSON."""
        default_dir = self._default_settings_dir()
        os.makedirs(default_dir, exist_ok=True)
        path, _ = QFileDialog.getOpenFileName(
            self, tr("Load Settings"), default_dir,
            "JSON Files (*.json);;All Files (*)")
        if not path:
            return
        if not os.path.exists(path):
            self.lbl_status.setText(
                "⚠ " + tr("File not found"))
            return
        try:
            self._load_settings(path)
            self.lbl_status.setText(
                "✅ " + tr("Settings loaded")
                + f": {os.path.basename(path)}")  # noqa: W503
        except Exception as e:
            self.lbl_status.setText(f"❌ {e}")

    def _save_settings(self, path=None):
        """Save dialog settings to JSON file."""
        import json
        from qgis.core import QgsMessageLog
        if path is None:
            path = os.path.join(self._default_settings_dir(),
                                "fsc_harvest_settings.json")
        data = {
            # ① Data Selection
            "main_layer": self.cmb_layer.currentText(),
            "fields": {k: c.currentText()
                       for k, c in self._field_combos.items()},
            "aux": [],
            # ③ Map Settings
            "title": self.txt_title.text(),
            "subtitle": self.txt_subtitle.text(),
            "org": self.txt_org.text(),
            "output_dir": self.txt_output.text(),
            "scale_auto": self.rad_auto.isChecked(),
            "scale_value": self.spn_scale.value(),
            "show_label": self.chk_show_label.isChecked(),
            "line_len": self.spn_line_len.value(),
            "num_sep": self.edt_num_sep.text(),
            "den_sep": self.edt_den_sep.text(),
            "num_fields": [c.currentText() for c in self._num_combos],
            "den_fields": [c.currentText() for c in self._den_combos],
            "sig_owner": self.chk_sig_owner.isChecked(),
            "sig_group": self.chk_sig_group.isChecked(),
            "group_rep_field": self.cmb_group_name.currentText(),
            "group_rep_name": self.txt_group_name.text().strip(),
            "show_legend": self.chk_legend.isChecked(),
            "separate_pdf": self.rad_separate.isChecked(),
            "open_after": self.chk_open_after.isChecked(),
            # ② Filters
            "filters": [c.currentText() for c in self._filter_fields],
        }
        # Aux layers: save checked + layer name
        for chk, cmb in zip(self._aux_checks, self._aux_combos):
            data["aux"].append({
                "checked": chk.isChecked(),
                "layer": cmb.currentText(),
            })
        crs = QgsProject.instance().crs()
        if crs.isValid():
            data["crs"] = crs.authid()
        try:
            os.makedirs(os.path.dirname(path), exist_ok=True)
            with open(path, "w", encoding="utf-8") as f:
                json.dump(data, f, ensure_ascii=False, indent=2)
        except Exception as e:
            QgsMessageLog.logMessage(
                f"FSC Settings save error: {e}", "LVT4U")

    def _load_settings(self, path=None):
        """Restore dialog settings from JSON file."""
        import json
        if path is None:
            path = os.path.join(self._default_settings_dir(),
                                "fsc_harvest_settings.json")
        if not os.path.exists(path):
            return
        try:
            with open(path, "r", encoding="utf-8") as f:
                data = json.load(f)
        except Exception:
            return

        # ① Main layer
        if data.get("main_layer"):
            idx = self.cmb_layer.findText(data["main_layer"])
            if idx >= 0:
                self.cmb_layer.setCurrentIndex(idx)
                # Trigger field population before restoring field values
                self._on_layer_changed(idx)

        # ① Field mappings
        fields = data.get("fields", {})
        for key, val in fields.items():
            cmb = self._field_combos.get(key)
            if cmb and val:
                idx = cmb.findText(val)
                if idx >= 0:
                    cmb.setCurrentIndex(idx)

        # ① Aux layers
        aux_list = data.get("aux", [])
        for i, aux_data in enumerate(aux_list):
            if i < len(self._aux_checks):
                self._aux_checks[i].setChecked(aux_data.get("checked", False))
                lyr_name = aux_data.get("layer", "")
                if lyr_name:
                    idx = self._aux_combos[i].findText(lyr_name)
                    if idx >= 0:
                        self._aux_combos[i].setCurrentIndex(idx)

        # ② Filters
        filters = data.get("filters", [])
        for i, val in enumerate(filters):
            if i < len(self._filter_fields) and val:
                idx = self._filter_fields[i].findText(val)
                if idx >= 0:
                    self._filter_fields[i].setCurrentIndex(idx)

        # ③ Map Settings
        if data.get("title"):
            self.txt_title.setText(data["title"])
        if data.get("subtitle"):
            self.txt_subtitle.setText(data["subtitle"])
        if data.get("org"):
            self.txt_org.setText(data["org"])
        if data.get("output_dir"):
            self.txt_output.setText(data["output_dir"])
        # Scale VALUE first, then radio
        if "scale_value" in data:
            self.spn_scale.setValue(data["scale_value"])
        if "scale_auto" in data:
            is_auto = data["scale_auto"]
            self.rad_auto.setChecked(is_auto)
            self.rad_custom.setChecked(not is_auto)
            self.spn_scale.setEnabled(not is_auto)
        if "show_label" in data:
            self.chk_show_label.setChecked(data["show_label"])
        if "line_len" in data:
            self.spn_line_len.setValue(data["line_len"])
        if "num_sep" in data:
            self.edt_num_sep.setText(data["num_sep"])
        if "den_sep" in data:
            self.edt_den_sep.setText(data["den_sep"])
        # Num/Den field combos
        for combos, key in [(self._num_combos, "num_fields"),
                            (self._den_combos, "den_fields")]:
            vals = data.get(key, [])
            for i, val in enumerate(vals):
                if i < len(combos) and val:
                    idx = combos[i].findText(val)
                    if idx >= 0:
                        combos[i].setCurrentIndex(idx)
        if "sig_owner" in data:
            self.chk_sig_owner.setChecked(data["sig_owner"])
        if "sig_group" in data:
            self.chk_sig_group.setChecked(data["sig_group"])
        if data.get("group_rep_field"):
            idx = self.cmb_group_name.findText(data["group_rep_field"])
            if idx >= 0:
                self.cmb_group_name.setCurrentIndex(idx)
        if data.get("group_rep_name"):
            self.txt_group_name.setText(data["group_rep_name"])
        if "show_legend" in data:
            self.chk_legend.setChecked(data["show_legend"])
        if "separate_pdf" in data:
            self.rad_separate.setChecked(data["separate_pdf"])
            self.rad_merged.setChecked(not data["separate_pdf"])
        if "open_after" in data:
            self.chk_open_after.setChecked(data["open_after"])
        if data.get("crs"):
            pass  # CRS now always uses project CRS

        # Auto-load features if main layer was restored
        if data.get("main_layer") and self._get_main_layer():
            self._load_features()

    def closeEvent(self, event):
        """Auto-save settings when dialog closes."""
        self._save_settings()
        super().closeEvent(event)
