# -*- coding: utf-8 -*-
"""TopoFix Dialog — Topology Check & Fix for Point, Line & Polygon layers.

Progressive-disclosure UX:
  State 1 (SCAN):    Layer selector + single Scan button
  State 2 (RESULTS): Only error types with count > 0 shown
  State 3 (FIX):     Relevant parameters + Fix button

Author: Loc Vu Trung tong hop vi dam me cung ban!
"""

import os
import tempfile

from qgis.core import (
    QgsWkbTypes,
    QgsProject, QgsVectorLayer, QgsGeometry, QgsFeature, QgsField,
    QgsCoordinateReferenceSystem, QgsCoordinateTransform)
from qgis.PyQt.QtCore import Qt, QVariant
from qgis.PyQt.QtGui import QColor, QFont
from qgis.PyQt.QtWidgets import (
    QDialog, QVBoxLayout, QHBoxLayout, QGridLayout,
    QGroupBox, QScrollArea, QWidget, QLabel,
    QComboBox, QPushButton, QCheckBox, QProgressBar,
    QDoubleSpinBox, QSpinBox, QMessageBox, QFrame,
    QApplication)

from ..shared.i18n import tr
from .algorithms.registry import (
    POLYGON_CHECKERS, LINE_CHECKERS, POINT_CHECKERS)
from .core import DEFAULT_PARAMS
from .core.workers import CheckWorker

# Highlight colors per algorithm
_COLORS = [
    QColor(255, 0, 0, 150),     # red
    QColor(255, 165, 0, 150),   # orange
    QColor(255, 255, 0, 150),   # yellow
    QColor(0, 255, 0, 150),     # green
    QColor(0, 255, 255, 150),   # cyan
    QColor(0, 0, 255, 150),     # blue
    QColor(128, 0, 128, 150),   # purple
    QColor(255, 0, 255, 150),   # magenta
    QColor(128, 128, 0, 150),   # olive
    QColor(0, 128, 128, 150),   # teal
    QColor(255, 100, 100, 150),  # light red
]

# Map algorithm name → which parameter keys it uses
_ALGO_PARAMS = {
    "gaps": ["gap_tolerance"],
    "sliver": ["sliver_max_area", "sliver_ratio"],
    "redundant": ["redundant_max_verts", "simplify_tol"],
    "must_not_overlap": ["mno_min_area"],
    "missing_nodes": ["missing_node_tol"],
    "tiny_polygon": ["tiny_min_area"],
}

# Param display info: key -> (label, unit, default, dec, min, max, is_int)
_PARAM_INFO = {
    "gap_tolerance": ("Gap tolerance", "ha", 0.001, 4, 0, 100, False),
    "sliver_max_area": ("Sliver max area", "ha", 1.0, 3, 0, 1000, False),
    "sliver_ratio": ("Sliver ratio P/\u221aA", "", 20.0, 1, 1, 100, False),
    "mno_min_area": ("Must-not-overlap min", "m\u00b2", 0.001, 4, 0, 100, False),
    "missing_node_tol": ("Missing node tolerance", "map units", 0.001, 6, 0, 1, False),
    "tiny_min_area": ("Tiny polygon min", "ha", 0.001, 4, 0, 100, False),
    "redundant_max_verts": ("Max vertices", "count", 500, 0, 10, 99999, True),
    "simplify_tol": ("Simplify tolerance", "map units", 0.1, 4, 0, 100, False),
}


class TopoFixDialog(QDialog):
    """Main dialog for TopoFix topology check & fix."""

    def __init__(self, iface, parent=None):
        super().__init__(parent or iface.mainWindow())
        self.iface = iface
        self.setWindowTitle("TopoFix — " + tr("Topology Check & Fix"))
        self.setMinimumSize(680, 320)
        self.resize(750, 500)
        self.setSizeGripEnabled(True)

        self._worker = None
        self._check_results = {}
        self._highlight_layers = []
        self._current_geom_type = "polygon"
        self._checkers = [cls() for cls in POLYGON_CHECKERS]

        self._build_ui()
        self._set_state("scan")

    # ----------------------------------------------------------
    # UI Construction
    # ----------------------------------------------------------

    def _build_ui(self):
        main = QVBoxLayout(self)
        main.setSpacing(8)

        # --- Layer selector ---
        grp_layer = QGroupBox(tr("Select layer"))
        lay_layer = QHBoxLayout(grp_layer)
        self.cb_layer = QComboBox()
        self.cb_layer.setMinimumWidth(300)
        btn_refresh = QPushButton(tr("Refresh"))
        btn_refresh.clicked.connect(self.refresh_layers)
        lay_layer.addWidget(self.cb_layer, 1)
        lay_layer.addWidget(btn_refresh)
        main.addWidget(grp_layer)

        # --- Scan button (prominent) ---
        self.btn_scan = QPushButton(f"🔍  {tr('Scan All Errors')}")
        self.btn_scan.setStyleSheet(
            "QPushButton { background: #1B5E20; color: white; "
            "font-size: 14px; font-weight: bold; padding: 10px 24px; "
            "border-radius: 6px; }"
            "QPushButton:hover { background: #2E7D32; }"
            "QPushButton:disabled { background: #999; }")
        self.btn_scan.clicked.connect(self._run_scan)
        main.addWidget(self.btn_scan)

        # --- Progress bar ---
        self.progress = QProgressBar()
        self.progress.setVisible(False)
        main.addWidget(self.progress)

        self.lbl_status = QLabel("")
        self.lbl_status.setAlignment(Qt.AlignCenter)
        main.addWidget(self.lbl_status)

        # --- Results section (hidden initially) ---
        self.grp_results = QGroupBox(tr("Errors Found"))
        self.grp_results.setVisible(False)
        scroll = QScrollArea()
        scroll.setWidgetResizable(True)
        scroll_w = QWidget()
        self.grid = QGridLayout(scroll_w)
        self.grid.setSpacing(4)

        # Header row
        bold = QFont()
        bold.setBold(True)
        for col, txt in enumerate([
                tr("Error type"), tr("Count"),
                tr("Highlight"), tr("Fix")]):
            lbl = QLabel(txt)
            lbl.setFont(bold)
            self.grid.addWidget(lbl, 0, col)

        # Pre-create all rows (hidden)
        self._row_widgets = []
        self._fix_checks = []
        self._highlight_btns = []
        self._result_labels = []

        for i, checker in enumerate(self._checkers):
            row = i + 1
            widgets = {}

            # Name label — use display_name_en for translation lookup
            name_lbl = QLabel(tr(checker.display_name_en))
            name_lbl.setStyleSheet(
                "color: #D32F2F; font-weight: bold;")
            self.grid.addWidget(name_lbl, row, 0)
            widgets["name"] = name_lbl

            # Count label
            cnt_lbl = QLabel("—")
            cnt_lbl.setAlignment(Qt.AlignCenter)
            self.grid.addWidget(cnt_lbl, row, 1)
            widgets["count"] = cnt_lbl
            self._result_labels.append(cnt_lbl)

            # Highlight button
            btn_show = QPushButton(tr("Show"))
            btn_show.setEnabled(False)
            btn_show.setFixedWidth(90)
            idx = i
            btn_show.clicked.connect(lambda _, x=idx: self._highlight(x))
            self.grid.addWidget(btn_show, row, 2)
            widgets["show"] = btn_show
            self._highlight_btns.append(btn_show)

            # Fix checkbox
            chk = QCheckBox()
            chk.setChecked(True)
            self.grid.addWidget(chk, row, 3, Qt.AlignCenter)
            widgets["fix"] = chk
            self._fix_checks.append(chk)

            self._row_widgets.append(widgets)

        scroll.setWidget(scroll_w)
        results_lay = QVBoxLayout(self.grp_results)
        results_lay.addWidget(scroll)
        main.addWidget(self.grp_results)

        # --- Parameters section (hidden initially) ---
        self.grp_params = QGroupBox(
            f"⚙ {tr('Advanced Parameters')}")
        self.grp_params.setCheckable(True)
        self.grp_params.setChecked(False)
        self.grp_params.setVisible(False)
        params_lay = QGridLayout(self.grp_params)

        self._spins = {}
        self._param_rows = {}
        prow = 0
        for key, (label, unit, default, dec, vmin, vmax, is_int) in \
                _PARAM_INFO.items():
            display = f"{tr(label)} ({unit})" if unit else tr(label)
            lbl = QLabel(display)
            if is_int:
                sp = QSpinBox()
                sp.setRange(int(vmin), int(vmax))
                sp.setValue(int(default))
            else:
                sp = QDoubleSpinBox()
                sp.setDecimals(dec)
                sp.setRange(vmin, vmax)
                sp.setValue(default)
            sp.setMinimumWidth(150)
            self._spins[key] = sp

            params_lay.addWidget(lbl, prow, 0)
            params_lay.addWidget(sp, prow, 1)
            self._param_rows[key] = (lbl, sp)
            prow += 1

        main.addWidget(self.grp_params)

        # --- Output CRS selector (always visible after results) ---
        self.grp_crs = QGroupBox(f"🌐 {tr('Output CRS')}")
        self.grp_crs.setVisible(False)
        crs_lay = QHBoxLayout(self.grp_crs)
        crs_lay.addWidget(QLabel(tr("Output CRS:")))
        self.cmb_output_crs = QComboBox()
        self.cmb_output_crs.setMinimumWidth(350)
        # First item: keep original
        self.cmb_output_crs.addItem(
            f"— {tr('Keep original CRS')} —", "")
        # Add VN-2000 options from shared list
        from .._crs_list import CRS_LIST
        for label, code in CRS_LIST:
            if not code:  # separator
                self.cmb_output_crs.addItem(f"── {label} ──", "")
                # Disable separator
                idx = self.cmb_output_crs.count() - 1
                self.cmb_output_crs.model().item(idx).setEnabled(False)
            else:
                self.cmb_output_crs.addItem(label, code)
        crs_lay.addWidget(self.cmb_output_crs, 1)
        main.addWidget(self.grp_crs)

        # --- Bottom action bar (hidden until results) ---
        self.action_bar = QFrame()
        action_lay = QHBoxLayout(self.action_bar)
        action_lay.setContentsMargins(0, 4, 0, 0)

        self.btn_fix = QPushButton(f"🔧 {tr('Fix Errors')}")
        self.btn_fix.setStyleSheet(
            "QPushButton { background: #E65100; color: white; "
            "font-size: 13px; font-weight: bold; padding: 8px 20px; "
            "border-radius: 5px; }"
            "QPushButton:hover { background: #F57C00; }"
            "QPushButton:disabled { background: #999; }")
        self.btn_fix.clicked.connect(self._run_fix)

        self.btn_clear = QPushButton(tr("Clear Highlight"))
        self.btn_clear.clicked.connect(self._clear_highlights)

        self.btn_stop = QPushButton(tr("Stop"))
        self.btn_stop.setEnabled(False)
        self.btn_stop.clicked.connect(self._stop)

        action_lay.addWidget(self.btn_fix)
        action_lay.addWidget(self.btn_clear)
        action_lay.addWidget(self.btn_stop)
        action_lay.addStretch()

        self.action_bar.setVisible(False)
        main.addWidget(self.action_bar)

        # --- Help button (ALWAYS visible) ---
        help_lay = QHBoxLayout()
        help_lay.setContentsMargins(0, 0, 0, 0)
        help_lay.addStretch()
        self.btn_help = QPushButton(f"❓ {tr('Help')}")
        self.btn_help.setToolTip(tr("Help"))
        self.btn_help.setFixedWidth(120)
        self.btn_help.clicked.connect(self._show_help)
        help_lay.addWidget(self.btn_help)
        main.addLayout(help_lay)

        # --- Motivational quote (fills empty space in scan state) ---
        self.lbl_quote = QLabel()
        self.lbl_quote.setText(
            '<div style="text-align:center; padding:30px 20px;">'
            '<p style="color:#2E7D32; font-size:15px; font-weight:bold; '
            'line-height:1.8;">'
            '💡 "Ai chẳng mắc lỗi một vài lần —<br>'
            'Hãy biết nhận lỗi, biết tu dưỡng<br>'
            'và biết sửa chữa kịp thời tránh tổn thất<br>'
            'là điều nên làm! Hãy tuân thủ pháp luật!"'
            '</p>'
            '<p style="color:#1B5E20; font-size:14px; font-weight:bold; '
            'margin-top:12px;">'
            '— LVT4U —'
            '</p>'
            '</div>'
        )
        self.lbl_quote.setAlignment(Qt.AlignCenter)
        self.lbl_quote.setWordWrap(True)
        main.addWidget(self.lbl_quote)

        main.addStretch()
        self.refresh_layers()

    # ----------------------------------------------------------
    # State management
    # ----------------------------------------------------------

    def _set_state(self, state):
        """Switch UI between scan / scanning / results states."""
        self._state = state

        if state == "scan":
            self.btn_scan.setEnabled(True)
            self.btn_scan.setVisible(True)
            self.progress.setVisible(False)
            self.grp_results.setVisible(False)
            self.grp_params.setVisible(False)
            self.grp_crs.setVisible(False)
            self.action_bar.setVisible(False)
            self.lbl_status.setText("")
            self.lbl_quote.setVisible(True)
            self.refresh_layers()

        elif state == "scanning":
            self.btn_scan.setEnabled(False)
            self.progress.setVisible(True)
            self.progress.setValue(0)
            self.btn_stop.setEnabled(True)
            self.lbl_quote.setVisible(False)

        elif state == "results":
            self.btn_scan.setText(f"🔄  {tr('Re-Scan')}")
            self.btn_scan.setEnabled(True)
            self.progress.setVisible(False)
            self.grp_results.setVisible(True)
            self.grp_crs.setVisible(True)
            self.action_bar.setVisible(True)
            self.btn_fix.setEnabled(True)
            self.btn_stop.setEnabled(False)
            self.lbl_quote.setVisible(False)

    # ----------------------------------------------------------
    # Layer selector
    # ----------------------------------------------------------

    def refresh_layers(self):
        self.cb_layer.clear()
        for lid, layer in QgsProject.instance().mapLayers().items():
            if isinstance(layer, QgsVectorLayer):
                gtype = layer.geometryType()
                if gtype == QgsWkbTypes.PolygonGeometry:
                    prefix = "🔷"
                elif gtype == QgsWkbTypes.LineGeometry:
                    prefix = "📏"
                elif gtype == QgsWkbTypes.PointGeometry:
                    prefix = "📍"
                else:
                    continue
                self.cb_layer.addItem(
                    f"{prefix} {layer.name()}", lid)

    def _get_layer(self):
        lid = self.cb_layer.currentData()
        if lid:
            return QgsProject.instance().mapLayer(lid)
        return None

    # ----------------------------------------------------------
    # Collect parameters
    # ----------------------------------------------------------

    def _params(self):
        p = dict(DEFAULT_PARAMS)
        for key, sp in self._spins.items():
            p[key] = sp.value()
        return p

    # ----------------------------------------------------------
    # Dynamic results grid (rebuilds for each geometry type)
    # ----------------------------------------------------------

    def _rebuild_results_grid(self):
        """Clear and rebuild the results grid for current checkers."""
        # Clear old grid rows (keep header row 0)
        for i in reversed(range(self.grid.count())):
            w = self.grid.itemAt(i).widget()
            if w:
                row, _, _, _ = self.grid.getItemPosition(i)
                if row > 0:  # Keep header
                    w.setParent(None)

        self._row_widgets = []
        self._fix_checks = []
        self._highlight_btns = []
        self._result_labels = []

        for i, checker in enumerate(self._checkers):
            row = i + 1
            widgets = {}

            name_lbl = QLabel(tr(checker.display_name_en))
            name_lbl.setStyleSheet(
                "color: #D32F2F; font-weight: bold;")
            self.grid.addWidget(name_lbl, row, 0)
            widgets["name"] = name_lbl

            cnt_lbl = QLabel("—")
            cnt_lbl.setAlignment(Qt.AlignCenter)
            self.grid.addWidget(cnt_lbl, row, 1)
            widgets["count"] = cnt_lbl
            self._result_labels.append(cnt_lbl)

            btn_show = QPushButton(tr("Show"))
            btn_show.setEnabled(False)
            btn_show.setFixedWidth(90)
            idx = i
            btn_show.clicked.connect(
                lambda _, x=idx: self._highlight(x))
            self.grid.addWidget(btn_show, row, 2)
            widgets["show"] = btn_show
            self._highlight_btns.append(btn_show)

            chk = QCheckBox()
            chk.setChecked(True)
            self.grid.addWidget(chk, row, 3, Qt.AlignCenter)
            widgets["fix"] = chk
            self._fix_checks.append(chk)

            self._row_widgets.append(widgets)

    # ----------------------------------------------------------
    # SCAN workflow (detects geometry type → selects algorithms)
    # ----------------------------------------------------------

    def _run_scan(self):
        layer = self._get_layer()
        if not layer:
            QMessageBox.warning(self, "TopoFix", tr("No layer selected"))
            return

        # Detect geometry type and select matching checkers
        gtype = layer.geometryType()
        if gtype == QgsWkbTypes.LineGeometry:
            self._current_geom_type = "line"
            checker_classes = LINE_CHECKERS
        elif gtype == QgsWkbTypes.PointGeometry:
            self._current_geom_type = "point"
            checker_classes = POINT_CHECKERS
        else:
            self._current_geom_type = "polygon"
            checker_classes = POLYGON_CHECKERS

        # Rebuild checkers and results grid for this geometry type
        self._checkers = [cls() for cls in checker_classes]
        self._rebuild_results_grid()

        self._set_state("scanning")

        # ============================================================
        # Step 1: Fix Geometries (native:fixgeometries, METHOD=1)
        # Sửa tự động mọi lỗi hình học trước khi scan topology
        # ============================================================
        self.lbl_status.setText(
            '<span style="color:#1B5E20; font-weight:bold;">'
            '⚙ [1/2] Fix Geometries — Đang sửa lỗi hình học...</span>')
        self.progress.setValue(10)
        QApplication.processEvents()

        self._fix_msg = ""
        try:
            import processing
            # Count invalid geometries BEFORE fix
            invalid_before = 0
            for feat in layer.getFeatures():
                geom = feat.geometry()
                if geom and not geom.isNull() and not geom.isGeosValid():
                    invalid_before += 1

            result = processing.run("native:fixgeometries",
                                    {"INPUT": layer,
                                     "METHOD": 1,
                                     "OUTPUT": "memory:"},
                                    feedback=None)
            fixed_layer = result["OUTPUT"]
            if fixed_layer and fixed_layer.isValid():
                self._scan_layer = fixed_layer
                if invalid_before > 0:
                    self._fix_msg = (
                        f'✅ Fix Geometries: đã sửa {invalid_before} '
                        f'geometry lỗi / {layer.featureCount()} features')
                else:
                    self._fix_msg = (
                        f'✅ Fix Geometries: {layer.featureCount()} '
                        f'features — không có lỗi hình học')
            else:
                self._scan_layer = layer
                self._fix_msg = "⚠ Fix Geometries: output không hợp lệ, dùng layer gốc"
        except Exception as e:
            self._scan_layer = layer
            self._fix_msg = f"⚠ Fix Geometries lỗi: {e}"

        self.progress.setValue(30)
        self.lbl_status.setText(
            f'<span style="color:#1565C0; font-weight:bold;">'
            f'🔍 [2/2] Scan Errors — Kiểm tra topology...</span>'
            f'<br/><span style="color:#666; font-size:11px;">'
            f'{self._fix_msg}</span>')
        QApplication.processEvents()

        # ============================================================
        # Step 2: Scan topology errors (11 algorithms)
        # ============================================================
        self._worker = CheckWorker(
            self._scan_layer, self._checkers, self._params())
        self._worker.progress.connect(self._on_progress)
        self._worker.finished.connect(self._on_scan_done)
        self._worker.error.connect(self._on_error)
        self._worker.start()

    def _on_scan_done(self, results):
        self._check_results = results
        errors_found = 0
        relevant_params = set()

        for i, checker in enumerate(self._checkers):
            ws = self._row_widgets[i]
            if checker.name in results:
                n = len(results[checker.name]["error_geometries"])
            else:
                n = 0

            if n > 0:
                # Show this row
                for w in ws.values():
                    w.setVisible(True)
                ws["count"].setText(
                    f'<span style="color:red"><b>{n}</b></span>')
                self._highlight_btns[i].setEnabled(True)
                self._fix_checks[i].setChecked(True)
                errors_found += n

                # Track relevant params
                key = checker.name.lower().replace(" ", "_")
                if key in _ALGO_PARAMS:
                    relevant_params.update(_ALGO_PARAMS[key])
            else:
                # Hide this row — no errors
                for w in ws.values():
                    w.setVisible(False)
                self._fix_checks[i].setChecked(False)

        # Show/hide params based on detected errors
        if relevant_params:
            self.grp_params.setVisible(True)
            for key, (lbl, sp) in self._param_rows.items():
                visible = key in relevant_params
                lbl.setVisible(visible)
                sp.setVisible(visible)
        else:
            self.grp_params.setVisible(False)

        self._set_state("results")

        # Build fix info line
        fix_line = (
            f'<br/><span style="color:#666; font-size:11px;">'
            f'{self._fix_msg}</span>' if self._fix_msg else "")

        if errors_found == 0:
            self.lbl_status.setText(
                f'<span style="color:green; font-size:14px">'
                f'✅ {tr("No errors found! Layer is clean.")}</span>'
                f'{fix_line}')
            self.grp_results.setVisible(False)
            self.action_bar.setVisible(False)
        else:
            self.lbl_status.setText(
                f'<span style="color:#E65100; font-size:13px">'
                f'⚠ {errors_found} {tr("errors found")}</span>'
                f'{fix_line}')

    # ----------------------------------------------------------
    # FIX workflow
    # ----------------------------------------------------------

    def _run_fix(self):
        layer = self._get_layer()
        if not layer:
            return

        reply = QMessageBox.question(
            self, "TopoFix",
            tr("Proceed with fixing? A temporary layer will be created."),
            QMessageBox.Yes | QMessageBox.No)
        if reply != QMessageBox.Yes:
            return

        selected = [self._checkers[i]
                    for i in range(len(self._checkers))
                    if self._fix_checks[i].isChecked()]

        if not selected:
            QMessageBox.warning(
                self, "TopoFix", tr("No fix types selected"))
            return

        # Export source to temp gpkg for fix pipeline
        tmp_dir = tempfile.mkdtemp(prefix="topofix_")
        current = os.path.join(tmp_dir, "step_input.gpkg")

        from qgis.core import QgsVectorFileWriter, QgsCoordinateTransformContext
        opts = QgsVectorFileWriter.SaveVectorOptions()
        opts.driverName = "GPKG"
        opts.forceMulti = True  # preserve ALL geometry types
        opts.includeZ = True    # preserve Z values if any
        # Exclude 'fid' field to avoid GPKG internal FID conflict
        exclude = []
        for i, field in enumerate(layer.fields()):
            if field.name().lower() == "fid":
                exclude.append(i)
        if exclude:
            opts.attributes = [i for i in range(layer.fields().count())
                               if i not in exclude]
        result = QgsVectorFileWriter.writeAsVectorFormatV3(
            layer, current, QgsCoordinateTransformContext(), opts)
        err = result if isinstance(result, int) else result[0]
        if err != QgsVectorFileWriter.NoError:
            QMessageBox.critical(
                self, "TopoFix", f"Export failed: {result}")
            return

        # SAFETY: verify exported count matches source
        src_count = layer.featureCount()
        chk = QgsVectorLayer(current, "_chk", "ogr")
        exp_count = chk.featureCount() if chk.isValid() else 0
        del chk
        if exp_count != src_count:
            QMessageBox.critical(
                self, "TopoFix",
                f"Export lost features: {src_count} → {exp_count}\n"
                f"Aborting to protect data.")
            import shutil
            shutil.rmtree(tmp_dir, ignore_errors=True)
            return

        # Run fix pipeline on MAIN THREAD (processing.run requires it)
        self.progress.setVisible(True)
        self.progress.setValue(0)
        self.btn_fix.setEnabled(False)
        params = self._params()

        # Count features BEFORE fix — this is the safety baseline
        in_count = src_count  # from export verification above

        for i, fixer in enumerate(selected):
            pct = int((i / len(selected)) * 100)
            self.progress.setValue(pct)
            self.lbl_status.setText(
                f"{tr('Fixing')}: {tr(fixer.display_name_en)}...")
            QApplication.processEvents()

            step_out = os.path.join(tmp_dir, f"step_{i}_out.gpkg")
            try:
                fixer.fix(current, step_out, params)
                if os.path.exists(step_out):
                    # SAFETY GUARD: count by iterating (not cached)
                    check = QgsVectorLayer(step_out, "_chk", "ogr")
                    if check.isValid():
                        out_count = sum(1 for _ in check.getFeatures())
                    else:
                        out_count = 0
                    del check
                    if out_count < in_count:
                        # REJECT this step — would delete features
                        self.lbl_status.setText(
                            f"⚠ {fixer.name}: {tr('Rejected')} "
                            f"({in_count}→{out_count})")
                        QApplication.processEvents()
                        continue
                    current = step_out
            except Exception as e:
                self.lbl_status.setText(f"⚠ {fixer.name}: {e}")
                QApplication.processEvents()

        self.progress.setValue(100)

        # FINAL SAFETY NET: verify final output count
        name = layer.name()
        file_layer = QgsVectorLayer(
            current, "_tmp_fixed", "ogr")
        if file_layer.isValid():
            final_count = sum(1 for _ in file_layer.getFeatures())
            if final_count < in_count:
                QMessageBox.critical(
                    self, "TopoFix",
                    f"SAFETY: Features lost ({in_count}→{final_count}).\n"
                    f"Fix aborted. Original data is safe.")
                del file_layer
                import shutil
                shutil.rmtree(tmp_dir, ignore_errors=True)
                self.btn_fix.setEnabled(True)
                self.progress.setVisible(False)
                return
            # Check if user wants CRS reprojection
            target_code = self.cmb_output_crs.currentData()
            src_crs = file_layer.crs()
            need_reproject = False
            if target_code:
                target_crs = QgsCoordinateReferenceSystem(target_code)
                if target_crs.isValid() and target_crs != src_crs:
                    need_reproject = True
                else:
                    target_crs = src_crs
            else:
                target_crs = src_crs

            # Detect actual geometry type from source
            wkb = file_layer.wkbType()
            # QgsWkbTypes removed — using QgsWkbTypes
            gtype_str = QgsWkbTypes.displayString(wkb)  # e.g. "MultiPolygon"
            if not gtype_str:
                gtype_str = "Polygon"

            # Create memory layer matching source geometry type
            crs_id = target_crs.authid()
            suffix = f"_{crs_id.replace(':', '')}" if need_reproject else ""
            mem = QgsVectorLayer(
                f"{gtype_str}?crs={crs_id}",
                f"{name}_TopoFixed{suffix}", "memory")
            mem_prov = mem.dataProvider()
            mem_prov.addAttributes(file_layer.fields().toList())
            mem.updateFields()

            # Copy ALL features, reprojecting if needed
            if need_reproject:
                xform = QgsCoordinateTransform(
                    src_crs, target_crs,
                    QgsProject.instance())
                new_feats = []
                for feat in file_layer.getFeatures():
                    nf = QgsFeature(feat)
                    geom = nf.geometry()
                    if geom and not geom.isNull():
                        geom.transform(xform)
                        nf.setGeometry(geom)
                    new_feats.append(nf)
                mem_prov.addFeatures(new_feats)
            else:
                feats = list(file_layer.getFeatures())
                mem_prov.addFeatures(feats)

            mem.updateExtents()
            # Copy renderer style
            if layer.renderer():
                mem.setRenderer(layer.renderer().clone())
            QgsProject.instance().addMapLayer(mem)
            self.refresh_layers()
            del file_layer  # release file lock

        # Cleanup temp files
        try:
            import shutil
            shutil.rmtree(tmp_dir, ignore_errors=True)
        except Exception:
            pass

        self._set_state("results")
        QMessageBox.information(
            self, "TopoFix",
            tr("Fix complete! Temporary layer added for review."
               " Re-Scan to verify, then save manually."))

    # ----------------------------------------------------------
    # Highlight errors
    # ----------------------------------------------------------

    def _highlight(self, idx):
        checker = self._checkers[idx]
        if checker.name not in self._check_results:
            return
        raw_geoms = self._check_results[checker.name]["error_geometries"]
        if not raw_geoms:
            return

        color = _COLORS[idx % len(_COLORS)]
        layer = self._get_layer()
        crs = layer.crs().authid() if layer else "EPSG:4326"

        # Normalize geometries: extract polygon/line/point parts from
        # GeometryCollections (intersection() can return mixed types)
        geoms = []
        for g in raw_geoms:
            if not g or g.isNull() or g.isEmpty():
                continue
            if g.type() == QgsWkbTypes.UnknownGeometry:
                # GeometryCollection — extract individual parts
                for part_idx in range(g.constGet().numGeometries()):
                    part = QgsGeometry(g.constGet().geometryN(part_idx).clone())
                    if part and not part.isNull() and not part.isEmpty():
                        geoms.append(part)
            else:
                geoms.append(g)

        if not geoms:
            QMessageBox.information(
                self, "TopoFix", tr("No displayable geometries"))
            return

        # Group by geometry type
        polys = [g for g in geoms
                 if g.type() == QgsWkbTypes.PolygonGeometry and g.area() > 0]
        lines = [g for g in geoms
                 if g.type() == QgsWkbTypes.LineGeometry]
        points = [g for g in geoms
                  if g.type() == QgsWkbTypes.PointGeometry]

        # Pick the most common type, prefer polygon
        if polys:
            display_geoms = polys
            gtype = "Polygon"
        elif lines:
            display_geoms = lines
            gtype = "LineString"
        elif points:
            display_geoms = points
            gtype = "Point"
        else:
            display_geoms = geoms
            sample = display_geoms[0]
            if sample.type() == QgsWkbTypes.PointGeometry:
                gtype = "Point"
            elif sample.type() == QgsWkbTypes.LineGeometry:
                gtype = "LineString"
            else:
                gtype = "Polygon"

        mem = QgsVectorLayer(
            f"{gtype}?crs={crs}", f"TopoFix_{checker.name}", "memory")
        prov = mem.dataProvider()
        prov.addAttributes([
            QgsField("No", QVariant.Int),
            QgsField("Error_Type", QVariant.String),
            QgsField("Area_m2", QVariant.Double),
        ])
        mem.updateFields()

        error_label = tr(checker.display_name_en)
        feats = []
        for i, g in enumerate(display_geoms, start=1):
            f = QgsFeature(mem.fields())
            f.setGeometry(g)
            area = g.area() if g.type() == QgsWkbTypes.PolygonGeometry else 0.0
            f.setAttributes([i, error_label, round(area, 4)])
            feats.append(f)
        prov.addFeatures(feats)

        # Style
        from qgis.core import (QgsSimpleFillSymbolLayer,
                               QgsSingleSymbolRenderer, QgsSymbol,
                               QgsSimpleMarkerSymbolLayer,
                               QgsSimpleLineSymbolLayer)
        sym = QgsSymbol.defaultSymbol(mem.geometryType())
        if gtype == "Point":
            sl = QgsSimpleMarkerSymbolLayer()
            sl.setColor(color)
            sl.setSize(4)
        elif gtype == "LineString":
            sl = QgsSimpleLineSymbolLayer()
            sl.setColor(color)
            sl.setWidth(1.5)
        else:
            sl = QgsSimpleFillSymbolLayer()
            sl.setColor(color)
            sl.setStrokeColor(QColor(0, 0, 0))
        sym.changeSymbolLayer(0, sl)
        mem.setRenderer(QgsSingleSymbolRenderer(sym))

        QgsProject.instance().addMapLayer(mem)
        self._highlight_layers.append(mem.id())
        self.iface.mapCanvas().setExtent(mem.extent())
        self.iface.mapCanvas().refresh()

    def _clear_highlights(self):
        proj = QgsProject.instance()
        for lid in self._highlight_layers:
            if proj.mapLayer(lid):
                proj.removeMapLayer(lid)
        self._highlight_layers.clear()
        self.iface.mapCanvas().refresh()

    # ----------------------------------------------------------
    # UI helpers
    # ----------------------------------------------------------

    def _on_progress(self, pct, msg):
        self.progress.setValue(pct)
        self.lbl_status.setText(msg)

    def _on_error(self, msg):
        self.lbl_status.setText(f"Error: {msg}")
        self._set_state("scan")

    def _stop(self):
        if self._worker:
            self._worker.cancel()
            self._set_state("scan")

    def _show_help(self):
        """Open help dialog with tabs: Algorithm + User Guide.
        Language follows plugin i18n setting."""
        from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout, QTabWidget,
                                         QTextBrowser, QPushButton,
                                         QHBoxLayout)
        from qgis.PyQt.QtCore import Qt
        from ..shared.i18n import current_language
        import os

        lang = current_language()  # "en" or "vi"

        dlg = QDialog(self)
        dlg.setWindowTitle("TopoFix — " + tr("Help"))
        dlg.setWindowFlags(
            dlg.windowFlags() & ~Qt.WindowContextHelpButtonHint)
        dlg.setMinimumSize(720, 500)
        dlg.resize(800, 650)
        lay = QVBoxLayout(dlg)

        tabs = QTabWidget()
        help_dir = os.path.join(
            os.path.dirname(__file__), "help")

        # Tab titles based on language
        tab1_title = ("📐 Nguyên lý & Thuật toán" if lang == "vi"
                      else "📐 Algorithm Principles")
        tab2_title = ("📖 Hướng dẫn sử dụng" if lang == "vi"
                      else "📖 User Guide")
        btn_text = "Đóng" if lang == "vi" else "Close"

        # Tab 1: Algorithm Principles
        browser1 = QTextBrowser()
        browser1.setOpenExternalLinks(True)
        algo_file = os.path.join(help_dir, "01_Algorithm_Principles.html")
        if os.path.exists(algo_file):
            with open(algo_file, "r", encoding="utf-8") as f:
                browser1.setHtml(f.read())
        else:
            browser1.setHtml("<p>File not found: 01_Algorithm_Principles.html</p>")
        tabs.addTab(browser1, tab1_title)

        # Tab 2: User Guide
        browser2 = QTextBrowser()
        browser2.setOpenExternalLinks(True)
        guide_file = os.path.join(help_dir, "02_User_Guide.html")
        if os.path.exists(guide_file):
            with open(guide_file, "r", encoding="utf-8") as f:
                browser2.setHtml(f.read())
        else:
            browser2.setHtml("<p>File not found: 02_User_Guide.html</p>")
        tabs.addTab(browser2, tab2_title)

        lay.addWidget(tabs)

        btn_lay = QHBoxLayout()
        btn_lay.addStretch()
        btn_close = QPushButton(btn_text)
        btn_close.clicked.connect(dlg.close)
        btn_lay.addWidget(btn_close)
        lay.addLayout(btn_lay)

        dlg.exec_()
