# -*- coding: utf-8 -*-
"""
LVT4U Plugin Suite — Menu Coordinator.

Creates the top-level "LVT4U" menu on the QGIS menu bar and registers
all sub-module actions. Each module is loaded on-demand when the user
clicks the corresponding menu item.

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

Scientific basis:
    - QGIS API Documentation (docs.qgis.org)
    - Cartographic standards for menu organization
"""

import contextlib
import os

from qgis.PyQt.QtCore import QSettings, QSize, Qt
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import (
    QAction, QMenu, QToolBar, QToolButton,
)

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


class LvtPlugin:
    """Top-level orchestrator for the LVT Plugin Suite.

    Responsibilities:
        - Create and manage the "LVT" menu on the QGIS menu bar
        - Register menu items for each module
        - Lazy-load module dialogs on first use
    """

    PLUGIN_NAME = "LVT4U"

    def __init__(self, iface):
        """Initialize the plugin.

        Args:
            iface: QgisInterface instance for interacting with the QGIS environment.
        """
        self.iface = iface
        self.plugin_dir = os.path.dirname(__file__)
        self.lvt_menu = None
        self.toolbar = None

        self.actions = []
        self.submenus = []

        # Module dialog instances (lazy-loaded)
        self._dialogs = {}
        self._basemap_submenu = None

    # ------------------------------------------------------------------
    # QGIS Plugin Lifecycle
    # ------------------------------------------------------------------

    def initGui(self):
        """Build the LVT menu and toolbar, insert into QGIS."""
        # First-run language selection
        self._first_run_language()
        # First-run disclaimer — MUST be accepted to activate plugin
        if not self._first_run_disclaimer():
            return

        self.lvt_menu = QMenu(self.PLUGIN_NAME)
        self.iface.mainWindow().menuBar().insertMenu(
            self.iface.firstRightStandardMenu().menuAction(),
            self.lvt_menu,
        )
        self._build_menu()
        self._build_toolbar()
        self._install_style_library()
        self._register_custom_crs()

        # Live language switching — rebuild UI immediately when language changes
        from .shared.i18n import language_signals
        language_signals.language_changed.connect(self._rebuild_ui)

    def _register_custom_crs(self):
        """Register LVT custom CRS into QGIS user CRS database.

        Makes VN-2000 Nội bộ, HN-72, Indian 1975 available globally
        in ALL QGIS CRS selectors — not just in LVT plugin dialogs.
        Runs once per plugin version; re-registers when version changes.
        """
        try:
            reg_key = 'LVT4U/crs_registry_version'
            # Read current plugin version from metadata.txt
            meta_path = os.path.join(self.plugin_dir, 'metadata.txt')
            current_version = '0'
            if os.path.isfile(meta_path):
                with open(meta_path, 'r', encoding='utf-8') as f:
                    for line in f:
                        if line.startswith('version='):
                            current_version = line.strip().split('=', 1)[1]
                            break

            stored = QSettings().value(reg_key, '')
            if stored == current_version:
                return  # Already registered for this version

            from ._crs_list import CRS_LIST
            from qgis.core import (
                QgsApplication, QgsCoordinateReferenceSystem,
            )
            import sqlite3

            db_path = QgsApplication.qgisUserDatabaseFilePath()
            if not db_path or not os.path.isfile(db_path):
                return

            conn = sqlite3.connect(db_path)
            cur = conn.cursor()

            # Remove old LVT entries to avoid duplicates
            cur.execute(
                "DELETE FROM tbl_srs WHERE description LIKE 'LVT | %'")

            # Find max srs_id for new entries
            cur.execute("SELECT MAX(srs_id) FROM tbl_srs")
            row = cur.fetchone()
            next_id = (row[0] or 100000) + 1

            registered = 0
            for label, code in CRS_LIST:
                # Skip separators (empty code) and EPSG entries
                if not code or code.startswith('EPSG:'):
                    continue

                # code is a proj4 string
                crs = QgsCoordinateReferenceSystem()
                crs.createFromProj(code)
                if not crs.isValid():
                    continue

                crs_name = f"LVT | {label}"
                wkt = crs.toWkt()

                cur.execute(
                    "INSERT INTO tbl_srs "
                    "(srs_id, description, projection_acronym, "
                    "ellipsoid_acronym, parameters, srid, "
                    "auth_name, auth_id, is_geo, deprecated, wkt) "
                    "VALUES (?, ?, 'tmerc', '', ?, ?, "
                    "'LVT', ?, 0, 0, ?)",
                    (next_id, crs_name, code, next_id,
                     next_id, wkt))
                next_id += 1
                registered += 1

            conn.commit()
            conn.close()

            QSettings().setValue(reg_key, current_version)

            # Refresh QGIS CRS database cache
            QgsCoordinateReferenceSystem.invalidateCache()

            if registered > 0:
                from qgis.core import Qgis
                QgsApplication.messageLog().logMessage(
                    f'LVT4U: Registered {registered} custom CRS '
                    f'(VN-2000 NB, HN-72, Indian 1975) v{current_version}',
                    'LVT4U', Qgis.MessageLevel.Info)
        except (AttributeError, ImportError, RuntimeError):
            pass  # nosec B110 — Silent fail: don't block plugin loading

    def unload(self):
        """Remove the LVT menu from the QGIS menu bar and clean up."""
        # Disconnect live-language signal first
        try:
            from .shared.i18n import language_signals
            language_signals.language_changed.disconnect(self._rebuild_ui)
        except (TypeError, RuntimeError):
            pass  # nosec B110

        # Close any open sub-dialogs
        for dlg in self._dialogs.values():
            try:
                dlg.close()
            except Exception:  # nosec B110
                pass  # nosec B110

        if self.lvt_menu:
            self.iface.mainWindow().menuBar().removeAction(
                self.lvt_menu.menuAction()
            )
            self.lvt_menu.deleteLater()
            self.lvt_menu = None

        try:
            if self.toolbar:
                import sip
                if not sip.isdeleted(self.toolbar):
                    self.toolbar.clear()
                    self.iface.mainWindow().removeToolBar(self.toolbar)
                    self.toolbar.setParent(None)
                    self.toolbar.deleteLater()
        except (RuntimeError, TypeError, Exception):
            pass  # C++ object already deleted by Qt
        self.toolbar = None

        self.actions.clear()
        self.submenus.clear()
        self._dialogs.clear()

    def _rebuild_ui(self, lang_code=None):
        """Rebuild menu + toolbar in-place when language changes.

        Called automatically via language_signals.language_changed.
        Closes any open sub-dialogs so they will reopen in the new
        language — no QGIS restart required.
        """
        # 1. Close all open sub-dialogs so they reopen with new language
        lang_name = {"vi": "Tiếng Việt", "en": "English"}.get(
            lang_code or "", lang_code or ""
        )
        closed_any = False
        for key, dlg in list(self._dialogs.items()):
            try:
                import sip
                if not sip.isdeleted(dlg) and dlg.isVisible():
                    dlg.close()
                    closed_any = True
            except Exception:  # nosec B110
                pass
        self._dialogs.clear()

        # 2. Tear down existing menu actions
        self.lvt_menu.clear()
        self.actions.clear()
        self.submenus.clear()

        # 3. Tear down toolbar
        try:
            if self.toolbar:
                import sip
                if not sip.isdeleted(self.toolbar):
                    self.toolbar.clear()
                    self.iface.mainWindow().removeToolBar(self.toolbar)
                    self.toolbar.setParent(None)
                    self.toolbar.deleteLater()
        except Exception:  # nosec B110
            pass  # nosec B110
        self.toolbar = None

        # 4. Rebuild both with new language
        self._build_menu()
        self._build_toolbar()

        # 5. Non-blocking notification
        try:
            from qgis.core import Qgis
            msg = (
                f"🌐 LVT4U: Language switched to {lang_name}. "
                f"Dialogs have been closed — please reopen them."
            ) if closed_any else (
                f"🌐 LVT4U: Language switched to {lang_name}."
            )
            self.iface.messageBar().pushMessage(
                "LVT4U",
                msg,
                level=Qgis.MessageLevel.Info,
                duration=4,
            )
        except Exception:  # nosec B110
            pass


    # ------------------------------------------------------------------
    # Menu Construction
    # ------------------------------------------------------------------

    def _icon(self, name):
        """Resolve an icon path from the icons/ directory.

        Tries SVG first (sharp at any size), then falls back to PNG.

        Args:
            name: Icon filename (e.g. "layout.png").

        Returns:
            QIcon instance, or empty QIcon if file not found.
        """
        base, _ = os.path.splitext(name)
        icons_dir = os.path.join(self.plugin_dir, "icons")
        # Prefer SVG for crisp rendering at any toolbar size
        for ext in (".svg", ".png"):
            path = os.path.join(icons_dir, base + ext)
            if os.path.isfile(path):
                return QIcon(path)
        return QIcon()

    def _add_action(self, icon_name, text, callback, menu=None):
        """Create a QAction and add it to the specified menu.

        Args:
            icon_name: Filename of the icon in icons/.
            text: Display text (will be translated).
            callback: Callable invoked when the action is triggered.
            menu: Target QMenu. Defaults to the top-level LVT menu.

        Returns:
            QAction: The created action.
        """
        action = QAction(
            self._icon(icon_name),
            tr(text),
            self.iface.mainWindow())
        action.triggered.connect(callback)
        target = menu if menu is not None else self.lvt_menu
        target.addAction(action)
        self.actions.append(action)
        return action

    def _add_submenu(self, icon_name, text):
        """Create a submenu under the LVT menu.

        Args:
            icon_name: Filename of the icon in icons/.
            text: Display text (will be translated).

        Returns:
            QMenu: The created submenu.
        """
        submenu = QMenu(tr(text), self.iface.mainWindow())
        submenu.setIcon(self._icon(icon_name))
        self.lvt_menu.addMenu(submenu)
        self.submenus.append(submenu)
        return submenu

    def _add_header(self, text):
        """Add a bold, non-clickable section header to the LVT4U menu.

        Mimics the native QGIS Processing menu group headers:
        disabled, bold QAction used purely as a visual separator label.

        Args:
            text: Header label text (untranslated; displayed as-is).
        """
        from qgis.PyQt.QtWidgets import QAction as _QAction
        hdr = _QAction(text, self.iface.mainWindow())
        hdr.setEnabled(False)
        font = hdr.font()
        font.setBold(True)
        hdr.setFont(font)
        self.lvt_menu.addAction(hdr)

    def _build_menu(self):
        """Assemble the LVT4U menu — flat native list, no submenus.

        All actions appear directly in the top-level LVT4U menu.
        Section headers use bold disabled QActions (QGIS Processing style).
        Basemap keeps a submenu because it contains too many entries.
        """

        # No custom stylesheet — inherits native QGIS/Qt theme.

        # ── Basemap (submenu — too many entries for flat list) ─────────
        basemap_sub = self._add_submenu("basemap.png", "Basemap")
        self._basemap_submenu = basemap_sub
        self._build_basemap_menu(basemap_sub)

        # ── CRS ────────────────────────────────────────────────────────
        self.lvt_menu.addSeparator()
        self._add_header("── CRS ──")
        self._add_action("crs.png",   "Coordinate System",    self._open_crs_tools)
        self._add_action("coord.png", "Coordinate Converter", self._open_coord_convert)
        self._add_action("font.png",  "Font Converter",       self._open_font)

        # ── Import ─────────────────────────────────────────────────────
        self.lvt_menu.addSeparator()
        self._add_header("── Import ──")
        self._add_action("excel.png",   "Excel → GIS",         self._open_excel_gis)
        self._add_action("dwg.png",     "CAD → SHP",           self._open_dwg_converter)
        self._add_action("mapinfo.svg", "MapInfo → SHP",       self._open_tab_reader)
        self._add_action("flycam.png",  "Flycam Photos",       self._open_flycam)
        self._add_action("satellite.png", "Satellite Downloader", self._open_satellite_dl)

        # ── Editing ────────────────────────────────────────────────────
        self.lvt_menu.addSeparator()
        self._add_header("── Editing ──")
        self._add_action("polyfix.png",       "TopoFix",            self._open_polyfix)
        self._add_action("split_multi.svg",   "Split Multipart",    self._open_split_multipart)
        self._add_action("thematic.png",      "Create Labels",      self._open_thematic_labels)
        self._add_action("layer_editor.png",  "Lot Numbering",      self._open_lot_numbering)
        self._add_action("layer_editor.png",  "Layer Editor",       self._open_layer_editor)
        self._add_action("click_classify.svg","Click Classify",     self._open_click_classify)
        self._add_action("layer_editor.png",  "Rename Field & Type",self._open_rename_field)
        self._add_action("polyfix.png",       "Disk Cleaner",       self._open_disk_cleaner)
        self._add_action("layer_editor.png",  "Layer Compare",      self._open_layer_compare)

        # ── Convert ────────────────────────────────────────────────────
        self.lvt_menu.addSeparator()
        self._add_header("── Convert ──")
        self._add_action("kml.png",     "SHP → KML/KMZ",  self._open_kml_export)
        self._add_action("kml.png",     "KML/KMZ → SHP",  self._open_kml_import)
        self._add_action("mbtiles.png", "MBTiles",         self._open_mbtiles)
        self._add_action("eudr.png",    "EUDR GeoJSON",    self._open_eudr)
        self._add_action("eudr.png",    "EUDR Documents",  self._open_eudr_reference)
        self._add_action("sync.png",    "LVT Sync",        self._open_sync)

        # ── Thematic ───────────────────────────────────────────────────
        self.lvt_menu.addSeparator()
        self._add_header("── Thematic ──")
        self._add_action("thematic.png",    "Create Thematic Map", self._open_thematic_tt16)
        self._add_action("layout.png",      "Map Layout",          self._open_layout)
        self._add_action("fsc_harvest.png", "FSC Harvest Map",     self._open_fsc_harvest)
        self._add_action("package.png",     "Đóng gói/Bung gói",  self._open_packager)
        with contextlib.suppress(ImportError, AttributeError):
            self._add_action("biodiversity.svg", "Biodiversity",   self._open_biodiversity)

        # ── Help ───────────────────────────────────────────────────────
        self.lvt_menu.addSeparator()
        self._add_header("── Help ──")
        self._add_action("legal.png", "Legal Documents", self._open_legal)
        self._add_action("legal.png", "Terms of Use",   self._open_terms)
        self._add_action("about.png", "About",           self._open_about)

    # ------------------------------------------------------------------
    # Basemap Registry
    # ------------------------------------------------------------------

    # Basemap catalog: (display_name, url, zmin, zmax)
    _BASEMAPS = {
        "Google": [
            ("Google Maps",
             "https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}",
             0,
             22),
            ("Google Satellite",
             "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}",
             0,
             22),
            ("Google Hybrid",
             "https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}",
             0,
             22),
            ("Google Terrain Hybrid",
             "https://mt1.google.com/vt/lyrs=p&x={x}&y={y}&z={z}",
             0,
             19),
        ],
        "Esri": [
            ("Esri Satellite",
             "https://server.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
             0,
             22),
            ("Esri Street",
             "https://server.arcgisonline.com/arcgis/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}",
             0,
             22),
            ("Esri Topographic",
             "https://server.arcgisonline.com/arcgis/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}",
             0,
             22),
            ("Esri National Geographic",
             "https://server.arcgisonline.com/arcgis/rest/services/NatGeo_World_Map/MapServer/tile/{z}/{y}/{x}",
             0,
             22),
            ("Esri Gray (light)",
             "https://server.arcgisonline.com/arcgis/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}",
             0,
             16),
            ("Esri Gray (dark)",
             "https://server.arcgisonline.com/arcgis/rest/services/Canvas/World_Dark_Gray_Base/MapServer/tile/{z}/{y}/{x}",
             0,
             16),
            ("Esri Physical",
             "https://server.arcgisonline.com/arcgis/rest/services/World_Physical_Map/MapServer/tile/{z}/{y}/{x}",
             0,
             22),
            ("Esri Shaded Relief",
             "https://server.arcgisonline.com/arcgis/rest/services/World_Shaded_Relief/MapServer/tile/{z}/{y}/{x}",
             0,
             13),
            ("Esri Terrain",
             "https://server.arcgisonline.com/arcgis/rest/services/World_Terrain_Base/MapServer/tile/{z}/{y}/{x}",
             0,
             13),
        ],
        "CartoDB": [
            ("Carto Light",
             "https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png",
             0,
             20),
            ("Carto Dark",
             "https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",
             0,
             20),
            ("Carto Antique",
             "https://cartocdn_a.global.ssl.fastly.net/base-antique/{z}/{x}/{y}.png",
             0,
             20),
            ("Carto Eco",
             "https://cartocdn_a.global.ssl.fastly.net/base-eco/{z}/{x}/{y}.png",
             0,
             20),
        ],
        "OpenStreetMap": [
            ("OpenStreetMap",
             "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
             0,
             19),
            ("OpenTopoMap",
             "https://tile.opentopomap.org/{z}/{x}/{y}.png",
             1,
             17),
        ],
        "Bing": [
            ("Bing VirtualEarth",
             "https://ecn.t3.tiles.virtualearth.net/tiles/a{q}.jpeg?g=1",
             0,
             22),
        ],
        "Other": [
            ("F4 Map 2D",
             "https://tile1.f4map.com/tiles/f4_2d/{z}/{x}/{y}.png",
             0,
             22),
        ],
    }

    def _build_basemap_menu(self, parent_menu):
        """Build flat basemap menu — one click to add any basemap.

        No custom stylesheet: inherits native QGIS/Qt theme for clean look.
        Group headers use separator + disabled bold action.
        """
        is_first = True
        for group_label, layers in self._BASEMAPS.items():
            # Add separator before every group (except the first)
            if not is_first:
                parent_menu.addSeparator()
            is_first = False
            # Group header — bold, not clickable
            sep_action = QAction(group_label, self.iface.mainWindow())
            sep_action.setEnabled(False)
            font = sep_action.font()
            font.setBold(True)
            sep_action.setFont(font)
            parent_menu.addAction(sep_action)
            for name, url, zmin, zmax in layers:
                action = QAction(
                    self._icon("basemap.png"), name,
                    self.iface.mainWindow(),
                )
                action.triggered.connect(
                    lambda checked, n=name, u=url, lo=zmin, hi=zmax:
                        self._add_xyz_basemap(n, u, lo, hi)
                )
                parent_menu.addAction(action)
                self.actions.append(action)

    # ------------------------------------------------------------------
    # Module Launchers (Lazy Loading)
    # ------------------------------------------------------------------

    def _open_layout(self):
        """Open the Map Layout dialog."""
        from .layout.dialog import LvtDialog

        if "layout" not in self._dialogs:
            layout_dir = os.path.join(self.plugin_dir, "layout")
            self._dialogs["layout"] = LvtDialog(self.iface, layout_dir)

        dlg = self._dialogs["layout"]
        dlg.refresh_layers()
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _open_fsc_harvest(self):
        """Open the FSC Plot Layout dialog."""
        from .fsc_harvest.dialog import FscHarvestDialog
        from qgis.PyQt.QtCore import QTimer

        if "fsc_harvest" not in self._dialogs:
            self._dialogs["fsc_harvest"] = FscHarvestDialog(
                self.iface, self.plugin_dir)

        dlg = self._dialogs["fsc_harvest"]
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()
        # Defer layer refresh until after dialog is painted
        QTimer.singleShot(80, lambda: dlg._refresh_layers(auto_load=False))

    def _open_kml_export(self):
        """Open the KML dialog (SHP to KML/KMZ tab)."""
        self._ensure_kml_dialog()
        dlg = self._dialogs["kml"]
        dlg.tabs.setCurrentIndex(0)
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _open_kml_import(self):
        """Open the KML dialog (KML/KMZ to SHP tab)."""
        self._ensure_kml_dialog()
        dlg = self._dialogs["kml"]
        dlg.tabs.setCurrentIndex(1)
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _ensure_kml_dialog(self):
        """Lazy-create the KML dialog instance."""
        if "kml" not in self._dialogs:
            from .kml.dialog import LvtKmlViewDialog
            self._dialogs["kml"] = LvtKmlViewDialog(
                self.iface, self.iface.mainWindow()
            )

    def _open_mbtiles(self):
        """Open the MBTiles creator dialog."""
        if "mbtiles" not in self._dialogs:
            from .mbtiles.dialog import MBTilesDialog
            self._dialogs["mbtiles"] = MBTilesDialog(self.iface)

        dlg = self._dialogs["mbtiles"]
        dlg.refresh_layers()
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _open_sync(self):
        """Open the LVT Sync dialog."""
        if "sync" not in self._dialogs:
            from .sync.dialog import LVTSyncDialog
            self._dialogs["sync"] = LVTSyncDialog(self.iface)

        dlg = self._dialogs["sync"]
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _open_eudr(self):
        """Open the EUDR GeoJSON exporter dialog."""
        if "eudr" not in self._dialogs:
            from .eudr.dialog import EudrExportDialog
            self._dialogs["eudr"] = EudrExportDialog(self.iface)

        dlg = self._dialogs["eudr"]
        dlg.refresh_layers()
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _open_eudr_reference(self):
        """Open the EUDR Reference Documents dialog."""
        if "eudr_ref" not in self._dialogs:
            from .eudr.reference_dialog import EudrReferenceDialog
            self._dialogs["eudr_ref"] = EudrReferenceDialog(self.iface)

        dlg = self._dialogs["eudr_ref"]
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _open_packager(self):
        """Open the Package/Extract dialog."""
        if "packager" not in self._dialogs:
            from .map_packager.dialog import PackageMapDialog
            self._dialogs["packager"] = PackageMapDialog(self.iface)

        dlg = self._dialogs["packager"]
        dlg.refresh_layers()
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _open_thematic_tt16(self):
        """Open the Thematic Map (TT16/2023) dialog."""
        if "tt16" not in self._dialogs:
            from .thematic_map.tt16_dialog import TT16Dialog
            self._dialogs["tt16"] = TT16Dialog(self.iface)

        dlg = self._dialogs["tt16"]
        dlg.refresh_layers()
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _open_thematic_tt33(self):
        """Open the Thematic Map (TT33) dialog."""
        self._show_placeholder("Thematic Map — TT 33")

    def _open_thematic_labels(self):
        """Open the standalone Plot Labels dialog."""
        if "plot_labels" not in self._dialogs:
            from .thematic_map.plot_labels import PlotLabelsDialog
            self._dialogs["plot_labels"] = PlotLabelsDialog(self.iface)

        dlg = self._dialogs["plot_labels"]
        dlg.refresh_layers()
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _open_layer_compare(self):
        """Open the Layer Compare (So sánh lớp bản đồ) dock panel."""
        from qgis.PyQt.QtCore import Qt
        if "layer_compare" not in self._dialogs:
            from .layer_compare.dialog import LayerCompareDialog
            dlg = LayerCompareDialog(self.iface)
            self.iface.mainWindow().addDockWidget(
                QtCompat.RightDockWidgetArea, dlg
            )
            self._dialogs["layer_compare"] = dlg

        dlg = self._dialogs["layer_compare"]
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _open_layer_editor(self):
        """Open the Layer Editor dialog."""
        from qgis.PyQt.QtCore import QTimer
        if "layer_editor" not in self._dialogs:
            from .layer_editor.dialog import LayerEditorDialog
            self._dialogs["layer_editor"] = LayerEditorDialog(self.iface)

        dlg = self._dialogs["layer_editor"]
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()
        # Defer refresh until after dialog is painted
        QTimer.singleShot(80, dlg.refresh_layers)

    def _open_click_classify(self):
        """Open the Click-Classify map tool dialog."""
        if "click_classify" not in self._dialogs:
            from .click_classify.dialog import ClickClassifyDialog
            self._dialogs["click_classify"] = ClickClassifyDialog(self.iface)
        dlg = self._dialogs["click_classify"]
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _open_rename_field(self):
        """Open the Rename Field & Type dialog."""
        from .rename_field.dialog import RenameFieldDialog
        # Always create fresh so it reflects current layers/language
        dlg = RenameFieldDialog(self.iface)
        self._dialogs["rename_field"] = dlg
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _open_lot_numbering(self):
        """Open the Lot Numbering (Đánh số lô) dialog."""
        from qgis.PyQt.QtCore import QTimer
        if "lot_numbering" not in self._dialogs:
            from .lot_numbering.dialog import LotNumberingDialog
            self._dialogs["lot_numbering"] = LotNumberingDialog(self.iface)

        dlg = self._dialogs["lot_numbering"]
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()
        QTimer.singleShot(80, dlg.refresh_layers)

    def _open_polyfix(self):
        """Open the TopoFix topology check & fix dialog."""
        if "polyfix" not in self._dialogs:
            from .poly_fix.dialog import TopoFixDialog
            self._dialogs["polyfix"] = TopoFixDialog(self.iface)

        dlg = self._dialogs["polyfix"]
        dlg.refresh_layers()
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _open_split_multipart(self):
        """Split selected multipart features into singlepart features.

        No dialog — runs inline and reports results via the message bar.
        Algorithm learned from the open-source 'splitmultipart' plugin
        by Alexandre Neto (GPL v2, 2013-2026).
        """
        from .split_multipart.split_multipart_tool import SplitMultipartTool
        SplitMultipartTool(self.iface).run()

    def _open_excel_gis(self):
        """Open the Excel → GIS importer dialog."""
        if "excel_gis" not in self._dialogs:
            from .excel_gis.dialog import ExcelGisDialog
            self._dialogs["excel_gis"] = ExcelGisDialog(self.iface)

        dlg = self._dialogs["excel_gis"]
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _open_dwg_converter(self):
        """Open the DWG/DXF to SHP converter dialog."""
        if "dwg_converter" not in self._dialogs:
            from .dwg_converter.dialog import DwgConverterDialog
            self._dialogs["dwg_converter"] = DwgConverterDialog(self.iface)

        dlg = self._dialogs["dwg_converter"]
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _open_tab_reader(self):
        """Open the MapInfo TAB → SHP converter dialog."""
        if "tab_reader" not in self._dialogs:
            from .tab_reader.dialog import TabReaderDialog
            self._dialogs["tab_reader"] = TabReaderDialog(self.iface)

        dlg = self._dialogs["tab_reader"]
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _open_disk_cleaner(self):
        """Open the Disk Cleaner dialog."""
        if "disk_cleaner" not in self._dialogs:
            from .disk_cleaner.dialog import DiskCleanerDialog
            self._dialogs["disk_cleaner"] = DiskCleanerDialog(self.iface)

        dlg = self._dialogs["disk_cleaner"]
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _open_legal(self):
        """Open the Legal Documents Library dialog."""
        if "legal" not in self._dialogs:
            from .legal_docs.dialog import LegalDocsDialog
            self._dialogs["legal"] = LegalDocsDialog(self.iface)

        dlg = self._dialogs["legal"]
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _open_font(self):
        """Open the Font Converter dialog."""
        if "font" not in self._dialogs:
            from .font_converter.dialog import FontConverterDialog
            self._dialogs["font"] = FontConverterDialog(self.iface)

        dlg = self._dialogs["font"]
        dlg.refresh_layers()
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _open_crs_tools(self):
        """Open the CRS Tools dialog."""
        if "crs" not in self._dialogs:
            from .crs_tools.dialog import CrsToolsDialog
            self._dialogs["crs"] = CrsToolsDialog(self.iface)

        dlg = self._dialogs["crs"]
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _open_coord_convert(self):
        """Open the Coordinate Converter dialog."""
        if "coord" not in self._dialogs:
            from .coord_convert.dialog import CoordConverterDialog
            self._dialogs["coord"] = CoordConverterDialog(self.iface)

        dlg = self._dialogs["coord"]
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _open_biodiversity(self):
        """Open the Biodiversity (Đa dạng sinh học) launcher dialog."""
        if "biodiversity" not in self._dialogs:
            from .biodiversity.dialog import BiodiversityDialog
            self._dialogs["biodiversity"] = BiodiversityDialog(
                self.iface, self.iface.mainWindow())

        dlg = self._dialogs["biodiversity"]
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _open_flycam(self):
        """Open the Flycam Photos dialog."""
        if "flycam" not in self._dialogs:
            from .flycam.dialog import FlycamDialog
            self._dialogs["flycam"] = FlycamDialog(self.iface)

        dlg = self._dialogs["flycam"]
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _open_satellite_dl(self):
        """Open the Satellite Image Downloader dialog."""
        if "satellite_dl" not in self._dialogs:
            from .satellite_dl.dialog import SatelliteDlDialog
            self._dialogs["satellite_dl"] = SatelliteDlDialog(self.iface)

        dlg = self._dialogs["satellite_dl"]
        dlg.refresh_layers()
        dlg.show()
        dlg.raise_()
        dlg.activateWindow()

    def _open_about(self):
        """Open the About dialog."""
        from .about.dialog import AboutDialog

        if "about" not in self._dialogs:
            self._dialogs["about"] = AboutDialog(self.iface, self.plugin_dir)
        self._dialogs["about"].exec()

    def _open_terms(self):
        """Open the Terms of Use dialog in read-only view mode (no accept/decline)."""
        self._show_disclaimer_dialog(accept_mode=False)

    # ------------------------------------------------------------------
    def _build_toolbar(self):
        """Create the LVT4U toolbar — modern big-icon popup buttons.

        Basemap keeps its flat submenu (many items).
        All other groups use LvtBigPopup: a floating 52px-icon grid
        that pops up below the toolbar button.
        """
        from .shared.big_popup import LvtBigPopup
        icons_dir = os.path.join(self.plugin_dir, "icons")

        self.toolbar = QToolBar("LVT4U", self.iface.mainWindow())
        self.toolbar.setObjectName("LVT4UToolbar")
        self.toolbar.setIconSize(QSize(22, 22))
        self.iface.addToolBar(self.toolbar)

        # Use QGIS default toolbar appearance (no custom stylesheet)


        def _toolbar_btn(icon_name, label):
            """Add a labelled icon button to the toolbar."""
            btn = QToolButton(self.toolbar)
            btn.setIcon(self._icon(icon_name))
            btn.setText(tr(label))
            btn.setToolTip(tr(label))
            btn.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
            btn.setIconSize(QSize(22, 22))
            self.toolbar.addWidget(btn)
            return btn

        def _popup_btn(icon_name, label, sections, cols=4):
            """Add a toolbar button that opens a LvtBigPopup on click."""
            btn = _toolbar_btn(icon_name, label)
            _popup_store = [None]   # use list so inner func can rebind

            def _show():
                # Rebuild popup each time so translations are live
                popup = LvtBigPopup(
                    parent=self.iface.mainWindow(),
                    title=tr(label),
                    sections=sections,
                    icons_dir=icons_dir,
                    cols=cols,
                )
                _popup_store[0] = popup
                from qgis.PyQt.QtGui import QCursor
                # Show just below the toolbar button
                btn_bottom = btn.mapToGlobal(
                    btn.rect().bottomLeft())
                popup.popup_at(btn_bottom)

            btn.clicked.connect(_show)
            return btn

        # ── Basemap (special: uses flat QMenu with many items) ─────────
        btn_basemap = _toolbar_btn("basemap.png", "Basemap")
        btn_basemap.clicked.connect(self._show_basemap_popup)

        self.toolbar.addSeparator()

        # ── CRS ────────────────────────────────────────────────────────
        _popup_btn("crs.png", "CRS", [
            ("COORDINATE SYSTEM", [
                ("crs.png",   "Coordinate System",    self._open_crs_tools),
                ("coord.png", "Coordinate Converter", self._open_coord_convert),
            ]),
            ("FONTS & TEXT", [
                ("font.png",  "Font Converter",        self._open_font),
            ]),
        ], cols=2)

        self.toolbar.addSeparator()

        # ── Import ─────────────────────────────────────────────────
        _popup_btn("excel.png", "Import", [
            ("SPREADSHEET & CAD", [
                ("excel.png",  "Excel → GIS",      self._open_excel_gis),
                ("dwg.png",    "CAD → SHP",        self._open_dwg_converter),
                ("mapinfo.svg","MapInfo → SHP",    self._open_tab_reader),
            ]),
            ("PHOTOS / IMAGERY", [
                ("flycam.png",    "Flycam",         self._open_flycam),
                ("satellite.png", "Satellite DL",   self._open_satellite_dl),
            ]),
        ], cols=3)

        self.toolbar.addSeparator()

        # ── Editing ────────────────────────────────────────────────────
        _popup_btn("layer_editor.png", "Editing", [
            ("VECTOR TOOLS", [
                ("polyfix.png",       "TopoFix",        self._open_polyfix),
                ("split_multi.svg",   "Split Multi",    self._open_split_multipart),
                ("thematic.png",      "Labels",         self._open_thematic_labels),
                ("layer_editor.png",  "Đánh số lô",     self._open_lot_numbering),
                ("layer_editor.png",  "Layer Editor",   self._open_layer_editor),
                ("click_classify.svg","Click Classify", self._open_click_classify),
                ("layer_editor.png",  "Rename Field",   self._open_rename_field),
            ]),
            ("TOOLS", [
                ("polyfix.png",      "Disk Cleaner",    self._open_disk_cleaner),
                ("layer_editor.png", "Layer Compare",   self._open_layer_compare),
            ]),
        ], cols=4)

        self.toolbar.addSeparator()

        # ── Convert ────────────────────────────────────────────────────
        _popup_btn("kml.png", "Convert", [
            ("KML / KMZ", [
                ("kml.png", "SHP → KML",    self._open_kml_export),
                ("kml.png", "KML → SHP",    self._open_kml_import),
            ]),
            ("TILES & DATA", [
                ("mbtiles.png", "MBTiles",  self._open_mbtiles),
                ("sync.png",    "LVT Sync", self._open_sync),
            ]),
            ("EUDR", [
                ("eudr.png", "EUDR GeoJSON",  self._open_eudr),
                ("eudr.png", "EUDR Docs",     self._open_eudr_reference),
            ]),
        ], cols=2)

        self.toolbar.addSeparator()

        # ── Thematic ───────────────────────────────────────────────────
        thematic_items = [
            ("THEMATIC MAPS", [
                ("thematic.png", "Thematic Map", self._open_thematic_tt16),
            ]),
            ("LAYOUT & EXPORT", [
                ("layout.png",      "Map Layout",           self._open_layout),
                ("fsc_harvest.png", "FSC Harvest",          self._open_fsc_harvest),
                ("package.png",     "Đóng gói/Bung gói",    self._open_packager),
            ]),
        ]
        with contextlib.suppress(ImportError, AttributeError):
            thematic_items.append(
                ("BIODIVERSITY", [
                    ("biodiversity.svg", "Biodiversity", self._open_biodiversity),
                ])
            )
        _popup_btn("thematic.png", "Thematic", thematic_items, cols=3)

        self.toolbar.addSeparator()

        # ── Help ───────────────────────────────────────────────────────
        _popup_btn("about.png", "Help", [
            ("REFERENCE", [
                ("legal.png", "Legal Docs",   self._open_legal),
                ("legal.png", "Terms of Use", self._open_terms),
                ("about.png", "About LVT4U",  self._open_about),
            ]),
        ], cols=3)


    def _show_basemap_popup(self):
        """Show basemap submenu as popup at toolbar button position."""
        if self._basemap_submenu:
            from qgis.PyQt.QtGui import QCursor
            self._basemap_submenu.popup(QCursor.pos())

    # ------------------------------------------------------------------
    # First-Run Language Selection
    # ------------------------------------------------------------------

    def _first_run_language(self):
        """Show language picker on first install."""
        key = "LVT4U/first_run_done"
        if QSettings().value(key, False, type=bool):
            return

        from qgis.PyQt.QtWidgets import QMessageBox
        from .shared.i18n import set_language

        # ── Qt5 → Qt6 compatibility shim (QGIS 4.x uses Qt6/PyQt6) ──────
        # Qt5: QMessageBox.Question / QMessageBox.ButtonRole.AcceptRole
        # Qt6: QMessageBox.Icon.Question / QMessageBox.ButtonRole.AcceptRole
        _q_icon_question = getattr(
            QMessageBox, 'Question',
            getattr(getattr(QMessageBox, 'Icon', None), 'Question', 4)
        )
        _q_accept_role = getattr(
            QMessageBox, 'AcceptRole',
            getattr(getattr(QMessageBox, 'ButtonRole', None), 'AcceptRole', 0)
        )
        # ─────────────────────────────────────────────────────────────────

        msg = QMessageBox(self.iface.mainWindow())
        msg.setWindowTitle("LVT4U — Language / Ngôn ngữ")
        msg.setText(
            "<h3>Welcome to LVT4U!</h3>"
            "<p>Please select your default language:<br>"
            "Vui lòng chọn ngôn ngữ mặc định:</p>"
        )
        msg.setIcon(_q_icon_question)
        msg.addButton("🇬🇧 English", _q_accept_role)
        btn_vn = msg.addButton("🇻🇳 Tiếng Việt", _q_accept_role)
        # exec_() removed in PyQt6 → use exec() with exec_() fallback
        if hasattr(msg, 'exec'):
            msg.exec()
        else:
            msg.exec()

        clicked = msg.clickedButton()
        if clicked == btn_vn:
            set_language("vi")
        else:
            set_language("en")

        QSettings().setValue(key, True)
        if clicked == btn_vn:
            self.iface.messageBar().pushSuccess(
                "LVT4U", "✅ Ngôn ngữ: Tiếng Việt — Chào mừng bạn!"
            )
        else:
            self.iface.messageBar().pushSuccess(
                "LVT4U", "✅ Language: English — Welcome!"
            )

    def _first_run_disclaimer(self):
        """Show Terms & Disclaimer on first install OR on version update.

        Returns True if user accepted, False if declined.
        The menu and toolbar are NOT built if this returns False.
        Shows on EVERY new plugin version.
        """
        # Use version-based key: shows again on each version update
        version = self._get_plugin_version()
        key = f'LVT4U/disclaimer_v{version.replace(".", "_")}'
        if QSettings().value(key, False, type=bool):
            return True

        accepted = self._show_disclaimer_dialog(accept_mode=True)
        if accepted:
            QSettings().setValue(key, True)
            self.iface.messageBar().pushSuccess(
                'LVT4U',
                '✅ Đã chấp nhận điều khoản — Plugin sẵn sàng! / Terms accepted — Ready!'
            )
            return True

        from qgis.PyQt.QtWidgets import QMessageBox
        QMessageBox.warning(
            self.iface.mainWindow(),
            'LVT4U — Không được kích hoạt / Not Activated',
            'Bạn đã từ chối điều khoản.\n'
            'LVT4U sẽ không được kích hoạt.\n\n'
            'Để sử dụng, vui lòng chấp nhận điều khoản.\n'
            '\nYou declined the terms.\n'
            'LVT4U will not be activated.\n\n'
            'To use it, please accept the terms.'
        )
        return False

    def _get_plugin_version(self):
        """Read plugin version from metadata.txt."""
        meta_path = os.path.join(self.plugin_dir, 'metadata.txt')
        if os.path.isfile(meta_path):
            try:
                with open(meta_path, encoding='utf-8') as f:
                    for line in f:
                        if line.startswith('version='):
                            return line.strip().split('=', 1)[1]
            except Exception:  # nosec B110
                pass  # nosec B110
        return 'unknown'

    def _show_disclaimer_dialog(self, accept_mode=True):
        """Show the Terms of Use dialog.

        Args:
            accept_mode (bool): If True, shows Accept/Decline buttons with checkbox
                                (first-run flow). If False, shows read-only with Close button.

        Returns:
            bool: True if accepted (accept_mode=True) or always True (accept_mode=False).
        """
        from qgis.PyQt.QtWidgets import (
            QDialog, QVBoxLayout, QHBoxLayout,
            QLabel, QTextEdit, QCheckBox, QPushButton
        )
        from qgis.PyQt.QtCore import Qt

        version = self._get_plugin_version()

        _DISCLAIMER_HTML = f"""
<style>
  body  {{ font-family: Arial, sans-serif; font-size: 12.5px; color: #212121; }}
  h2    {{ color: #1B5E20; font-size: 15px; margin: 6px 0 4px 0; }}
  h3    {{ color: #0D47A1; font-size: 13px; margin: 14px 0 4px 0;
          border-bottom: 1px solid #BBDEFB; padding-bottom: 3px; }}
  .vi   {{ border-left: 4px solid #1B5E20; padding: 4px 8px; margin: 4px 0;
          background: #F1F8E9; border-radius: 0 4px 4px 0; }}
  .en   {{ border-left: 4px solid #0D47A1; padding: 4px 8px; margin: 4px 0;
          background: #E3F2FD; border-radius: 0 4px 4px 0; }}
  .warn {{ background: #FFF3E0; border: 1px solid #FF6F00;
          padding: 8px; border-radius: 4px; margin: 10px 0; }}
  .stop {{ background: #FFEBEE; border: 1px solid #C62828;
          padding: 8px; border-radius: 4px; margin: 10px 0; }}
  .ok   {{ background: #E8F5E9; border: 1px solid #2E7D32;
          padding: 8px; border-radius: 4px; margin: 10px 0; }}
  table {{ border-collapse: collapse; width: 100%; font-size: 11.5px; margin: 6px 0; }}
  th    {{ background: #1B5E20; color: white; padding: 5px 8px; text-align: left; }}
  td    {{ border: 1px solid #C8E6C9; padding: 4px 8px; vertical-align: top; }}
  tr:nth-child(even) td {{ background: #F9FBE7; }}
  ul    {{ margin: 3px 0; padding-left: 18px; }}
  li    {{ margin: 2px 0; }}
  b.red {{ color: #C62828; }}
  b.grn {{ color: #1B5E20; }}
</style>

<h2>&#127807; LVT4U Plugin v{version} &mdash; &#272;i&#7873;u kho&#7843;n s&#7917; d&#7909;ng / Terms of Use</h2>

<h3>1. &#128218; M&#7909;c &#273;&#237;ch phi th&#432;&#417;ng m&#7841;i / Non-Commercial &amp; Educational Purpose Only</h3>

<div class='stop'>
&#128683; <b>QUAN TR&#7885;NG / IMPORTANT</b><br>
<b>&#127760; VI:</b> Plugin LVT4U &#273;&#432;&#7907;c x&#226;y d&#7921;ng v&#7899;i m&#7909;c &#273;&#237;ch
<b style='color:#C62828'>H&#7885;C T&#7852;P, TRAO &#272;&#7892;I KINH NGHI&#7878;M V&Agrave; NGHI&#202;N C&#7912;U PHI TH&#431;&#416;NG M&#7840;I</b>
trong l&#297;nh v&#7921;c GIS v&agrave; tr&#7855;c &#273;&#7883;a b&#7843;n &#273;&#7891;.<br><br>
<b>&#127468;&#127463; EN:</b> LVT4U plugin is built for
<b style='color:#C62828'>EDUCATIONAL, KNOWLEDGE-SHARING AND NON-COMMERCIAL RESEARCH</b>
purposes only in the fields of GIS and geodetic mapping.
</div>

<div class='vi'>
<b>&#127760; Ti&#7871;ng Vi&#7879;t:</b>
<ul>
  <li>Plugin n&agrave;y <b>KH&Ocirc;NG</b> &#273;&#432;&#7907;c thi&#7871;t k&#7871;, ch&#7913;ng nh&#7853;n ho&#7863;c ph&ecirc; duy&#7879;t cho m&#7909;c &#273;&#237;ch th&#432;&#417;ng m&#7841;i.</li>
  <li>Kh&ocirc;ng &#273;&#432;&#7907;c s&#7917; d&#7909;ng k&#7871;t qu&#7843; t&#7915; plugin n&agrave;y trong c&aacute;c d&#7921; &aacute;n <b>thu ph&#237;, h&#7907;p &#273;&#7891;ng kinh t&#7871;, l&#7853;p h&#7891; s&#417; ph&aacute;p l&yacute; &#273;&#7845;t &#273;ai</b> hay b&#7845;t k&#7923; ho&#7841;t &#273;&#7897;ng kinh doanh c&oacute; l&#7907;i nhu&#7853;n n&agrave;o.</li>
  <li>T&aacute;c gi&#7843; <b>t&#7915; ch&#7889;i m&#7885;i tr&aacute;ch nhi&#7879;m ph&aacute;p l&yacute;</b> n&#7871;u plugin b&#7883; s&#7917; d&#7909;ng cho m&#7909;c &#273;&#237;ch th&#432;&#417;ng m&#7841;i ho&#7863;c ngo&agrave;i ph&#7841;m vi h&#7885;c t&#7853;p.</li>
  <li>Vi&#7879;c s&#7917; d&#7909;ng th&#432;&#417;ng m&#7841;i <b>vi ph&#7841;m &#273;i&#7873;u kho&#7843;n n&agrave;y</b> v&agrave; ho&agrave;n to&agrave;n thu&#7897;c tr&aacute;ch nhi&#7879;m c&#7911;a ng&#432;&#7901;i d&ugrave;ng.</li>
</ul>
</div>
<div class='en'>
<b>&#127468;&#127463; English:</b>
<ul>
  <li>This plugin is <b>NOT</b> designed, certified or approved for commercial purposes.</li>
  <li>Do not use plugin outputs in <b>paid projects, commercial contracts, land registry dossiers</b> or any profit-making activities.</li>
  <li>The author <b>disclaims all legal liability</b> if the plugin is used commercially or beyond educational scope.</li>
  <li>Commercial use <b>violates these terms</b> and is entirely the user&apos;s responsibility.</li>
</ul>
</div>

<h3>2. &#128302; &#272;&#7897; ch&#237;nh x&aacute;c theo t&#7915;ng module / Per-Module Accuracy Specification</h3>

<div class='warn'>&#9888;&#65039; <b>VI:</b> M&#7895;i module c&oacute; gi&#7899;i h&#7841;n &#273;&#7897; ch&#237;nh x&aacute;c ri&ecirc;ng. Ng&#432;&#7901;i d&ugrave;ng c&#7847;n hi&#7875;u r&#245; tr&#432;&#7899;c khi s&#7917; d&#7909;ng.<br>
&#9888;&#65039; <b>EN:</b> Each module has its own accuracy limitations. Users must understand these before use.</div>

<table>
  <tr><th>Module</th><th>&#127760; M&ocirc; t&#7843; &amp; Gi&#7899;i h&#7841;n</th><th>&#127468;&#127463; Description &amp; Limits</th></tr>
  <tr>
    <td><b>KML / KMZ Export</b></td>
    <td>Ch&ecirc;nh l&#7879;ch t&#7885;a &#273;&#7897; &plusmn;0.1&ndash;3m t&ugrave;y file datum NTv2. Ph&#7909; thu&#7897;c ph&eacute;p chi&#7871;u ngu&#7891;n.</td>
    <td>Coordinate offset &plusmn;0.1&ndash;3m depending on NTv2 datum grid. Depends on source projection.</td>
  </tr>
  <tr>
    <td><b>KML &rarr; SHP</b></td>
    <td>Ph&eacute;p chi&#7871;u ng&#432;&#7907;c WGS-84 &rarr; VN-2000 ph&#7909; thu&#7897;c datum grid. C&oacute; th&#7875; sai l&#7879;ch &plusmn;1&ndash;5m n&#7871;u thi&#7871;u grid.</td>
    <td>Inverse projection WGS-84 &rarr; VN-2000 depends on datum grid. Up to &plusmn;1&ndash;5m without grid.</td>
  </tr>
  <tr>
    <td><b>T&#237;nh di&#7879;n t&#237;ch / Area</b></td>
    <td>Ch&#7881; mang t&#237;nh tham kh&#7843;o. C&#7847;n d&ugrave;ng CRS ph&ugrave; h&#7907;p (UTM/VN-2000).</td>
    <td>For reference only. Use appropriate CRS (UTM/VN-2000) for accurate results.</td>
  </tr>
  <tr>
    <td><b>Thematic Map / TT16</b></td>
    <td>B&#7843;n &#273;&#7891; ch&#7911; &#273;&#7873; ph&#7909;c v&#7909; h&#7885;c t&#7853;p, tr&igrave;nh b&agrave;y. Kh&ocirc;ng thay th&#7871; b&#7843;n &#273;&#7891; ph&aacute;p l&yacute; ch&#237;nh th&#7913;c.</td>
    <td>Thematic maps for educational/presentation use only. Cannot replace official legal maps.</td>
  </tr>
  <tr>
    <td><b>MBTiles / Basemap</b></td>
    <td>Tile n&#7873;n ph&#7909;c v&#7909; hi&#7875;n th&#7883;. B&#7843;n quy&#7873;n &#7843;nh thu&#7897;c nh&agrave; cung c&#7845;p (Google, OSM...). Kh&ocirc;ng d&ugrave;ng th&#432;&#417;ng m&#7841;i.</td>
    <td>Background tiles for display only. Imagery copyright belongs to providers. Not for commercial use.</td>
  </tr>
  <tr>
    <td><b>Satellite Download</b></td>
    <td>&#7842;nh v&#7879; tinh tu&acirc;n theo &#273;i&#7873;u kho&#7843;n nh&agrave; cung c&#7845;p (GEE, Sentinel...). Ch&#7881; nghi&ecirc;n c&#7913;u.</td>
    <td>Imagery subject to provider terms (GEE, Sentinel...). Research purposes only.</td>
  </tr>
  <tr>
    <td><b>CAD Converter / DWG</b></td>
    <td>Chuy&#7875;n &#273;&#7893;i CAD&rarr;SHP c&oacute; th&#7875; m&#7845;t th&ocirc;ng tin topology, nh&atilde;n, block. K&#7871;t qu&#7843; c&#7847;n ki&#7875;m tra th&#7911; c&ocirc;ng.</td>
    <td>CAD&rarr;SHP may lose topology, labels, blocks. Results require manual verification.</td>
  </tr>
  <tr>
    <td><b>CRS Tools / Coord Convert</b></td>
    <td>C&ocirc;ng c&#7909; h&#7885;c t&#7853;p. &#272;&#7897; ch&#237;nh x&aacute;c &plusmn;0.5&ndash;2m t&ugrave;y datum v&agrave; thu&#7853;t to&aacute;n Helmert.</td>
    <td>Learning tools. Accuracy &plusmn;0.5&ndash;2m depending on datum and Helmert algorithm.</td>
  </tr>
  <tr>
    <td><b>EUDR / FSC Export</b></td>
    <td>Bi&#7875;u m&#7851;u xu&#7845;t ph&#7909;c v&#7909; h&#7885;c t&#7853;p. Kh&ocirc;ng thay th&#7871; h&#7879; th&#7889;ng EUDR ch&#237;nh th&#7913;c c&#7911;a EU.</td>
    <td>Export forms for practice only. Not a replacement for official EU EUDR system.</td>
  </tr>
  <tr>
    <td><b>Excel / GIS Integration</b></td>
    <td>Ph&#7909; thu&#7897;c &#273;&#7883;nh d&#7841;ng file v&agrave; m&atilde; h&oacute;a k&yacute; t&#7921;. C&#7847;n ki&#7875;m tra d&#7919; li&#7879;u &#273;&#7847;u ra.</td>
    <td>Depends on file format and encoding. Output data requires verification.</td>
  </tr>
</table>

<h3>3. &#127951; Cam k&#7871;t b&aacute;m theo ti&ecirc;u chu&#7849;n Vi&#7879;t Nam / Commitment to Vietnamese Standards</h3>

<div class='ok'>&#10003; <b>VI:</b> T&aacute;c gi&#7843; <b>lu&ocirc;n n&#7895; l&#7921;c</b> x&acirc;y d&#7921;ng v&agrave; c&#7853;p nh&#7853;t plugin b&aacute;m theo quy &#273;&#7883;nh c&#7911;a:<br>
&#10003; <b>EN:</b> The author <b>always strives</b> to build and update the plugin following:
</div>
<div class='vi'><b>&#127760; Ti&#7871;ng Vi&#7879;t:</b>
<ul>
  <li><b>B&#7897; T&agrave;i nguy&ecirc;n v&agrave; M&ocirc;i tr&#432;&#7901;ng (BTNMT)</b> &mdash; Quy chu&#7849;n k&#7929; thu&#7853;t qu&#7889;c gia v&#7873; &#273;o &#273;&#7841;c, b&#7843;n &#273;&#7891; &#273;&#7883;a ch&#237;nh</li>
  <li><b>Th&ocirc;ng t&#432; 25/2014/TT-BTNMT</b> &mdash; B&#7843;n &#273;&#7891; &#273;&#7883;a ch&#237;nh</li>
  <li><b>Th&ocirc;ng t&#432; 16/2021/TT-BTNMT (TT16)</b> &mdash; K&yacute; hi&#7879;u b&#7843;n &#273;&#7891; &#273;&#7883;a h&igrave;nh t&#7881; l&#7879; l&#7899;n</li>
  <li><b>TCVN</b> v&#7873; GIS, tr&#7855;c &#273;&#7883;a, ph&eacute;p chi&#7871;u VN-2000, HN-72</li>
  <li><b>H&#7879; t&#7885;a &#273;&#7897; VN-2000</b> theo Quy&#7871;t &#273;&#7883;nh 83/2000/Q&#272;-TTg</li>
</ul>
<i>&#272;&acirc;y l&agrave; n&#7895; l&#7921;c phi th&#432;&#417;ng m&#7841;i. K&#7871;t qu&#7843; ch&#432;a &#273;&#432;&#7907;c coi l&agrave; &#273;&#7841;t chu&#7849;n ph&aacute;p l&yacute; n&#7871;u ch&#432;a c&oacute; x&aacute;c nh&#7853;n c&#7911;a c&#417; quan nh&agrave; n&#432;&#7899;c.</i>
</div>
<div class='en'><b>&#127468;&#127463; English:</b>
<ul>
  <li><b>Ministry of Natural Resources and Environment (MONRE)</b> &mdash; National standards for surveying and cadastral mapping</li>
  <li><b>Circular 25/2014/TT-BTNMT</b> &mdash; Cadastral maps</li>
  <li><b>Circular 16/2021/TT-BTNMT (TT16)</b> &mdash; Large-scale topographic map symbols</li>
  <li><b>TCVN</b> for GIS, geodesy, VN-2000 and HN-72 projections</li>
  <li><b>VN-2000</b> coordinate system per Decision 83/2000/QD-TTg</li>
</ul>
<i>This is a non-commercial effort. Results must not be considered officially compliant without verification by a competent state authority.</i>
</div>

<h3>4. &#128220; B&#7843;n quy&#7873;n &amp; Gi&#7845;y ph&eacute;p / Copyright &amp; License</h3>
<div class='vi'><b>&#127760;:</b> Plugin LVT4U &#273;&#432;&#7907;c ph&aacute;t h&agrave;nh <b>mi&#7877;n ph&#237;</b> theo gi&#7845;y ph&eacute;p <b>GPL-3.0</b>. Quy&#7873;n t&aacute;c gi&#7843; thu&#7897;c v&#7873; <b>L&#7897;c V&#361; Trung (LVTCenter)</b>. Cho ph&eacute;p s&#7917; d&#7909;ng, chia s&#7867;, ch&#7881;nh s&#7917;a theo GPL-3.0 &mdash; <b>kh&ocirc;ng &#273;&#432;&#7907;c d&ugrave;ng cho m&#7909;c &#273;&#237;ch th&#432;&#417;ng m&#7841;i</b>.</div>
<div class='en'><b>&#127468;&#127463;:</b> LVT4U is released <b>free</b> under <b>GPL-3.0</b>. Copyright &copy; <b>L&#7897;c V&#361; Trung (LVTCenter)</b>. Use, share and modify under GPL-3.0 &mdash; <b>commercial use is strictly prohibited</b>.</div>

<h3>5. &#9878;&#65039; Mi&#7877;n tr&#7915; tr&aacute;ch nhi&#7879;m / Disclaimer of Liability</h3>
<div class='vi'><b>&#127760; T&aacute;c gi&#7843; KH&Ocirc;NG ch&#7883;u tr&aacute;ch nhi&#7879;m v&#7873;:</b>
<ul>
  <li>Sai l&#7879;ch t&#7885;a &#273;&#7897;, di&#7879;n t&#237;ch, h&igrave;nh h&#7885;c do gi&#7899;i h&#7841;n thu&#7853;t to&aacute;n ho&#7863;c d&#7919; li&#7879;u &#273;&#7847;u v&agrave;o</li>
  <li>Thi&#7879;t h&#7841;i ph&aacute;t sinh khi d&ugrave;ng k&#7871;t qu&#7843; plugin trong c&ocirc;ng vi&#7879;c th&#432;&#417;ng m&#7841;i, ph&aacute;p l&yacute;, h&agrave;nh ch&#237;nh</li>
  <li>M&#7845;t d&#7919; li&#7879;u, h&#7887;ng file, xung &#273;&#7897;t ph&#7847;n m&#7873;m trong qu&aacute; tr&igrave;nh s&#7917; d&#7909;ng</li>
  <li>B&#7845;t k&#7923; h&#7853;u qu&#7843; n&agrave;o do ng&#432;&#7901;i d&ugrave;ng s&#7917; d&#7909;ng sai m&#7909;c &#273;&#237;ch so v&#7899;i &#273;i&#7873;u kho&#7843;n n&agrave;y</li>
</ul>
</div>
<div class='en'><b>&#127468;&#127463; The author is NOT liable for:</b>
<ul>
  <li>Coordinate, area or geometry errors due to algorithm limitations or input data</li>
  <li>Damages from using plugin results in commercial, legal or administrative work</li>
  <li>Data loss, file corruption or software conflicts during use</li>
  <li>Any consequences from using the plugin outside the scope of these terms</li>
</ul>
</div>

<div class='warn'>&#9888;&#65039; <b>K&#7929; thu&#7853;t / Technical Note:</b><br>
<b>&#127760;:</b> Ch&ecirc;nh l&#7879;ch t&#7885;a &#273;&#7897; gi&#7917;a c&aacute;c ph&eacute;p chi&#7871;u (VN-2000, WGS-84, HN-72...) l&agrave; hi&#7879;n t&#432;&#7907;ng <b>k&#7929; thu&#7853;t b&igrave;nh th&#432;&#7901;ng</b>. K&#7871;t qu&#7843; ph&#7909; thu&#7897;c v&agrave;o file l&#432;&#7899;i datum (NTv2 grid) c&#7911;a QGIS b&#7841;n.<br>
<b>&#127468;&#127463;:</b> Coordinate differences between projections are a <b>normal technical phenomenon</b>. Results depend on the datum grid files (NTv2) installed in your QGIS.
</div>
"""

        dlg = QDialog(self.iface.mainWindow())
        title = ('LVT4U v' + version +  # noqa: W504
                 ' — Điều khoản sử dụng / Terms of Use')
        dlg.setWindowTitle(title)
        dlg.setMinimumSize(720, 600)
        _Qt_NCHB = getattr(Qt, 'WindowContextHelpButtonHint',
                           getattr(getattr(Qt, 'WindowType', Qt),
                                   'WindowContextHelpButtonHint', 0))
        dlg.setWindowFlags(dlg.windowFlags() & ~_Qt_NCHB)

        lay = QVBoxLayout(dlg)

        hdr = QLabel(
            f'<b>&#127807; LVT4U v{version}</b> &nbsp;|&nbsp; '
            f'<i>Phi th&#432;&#417;ng m&#7841;i — Non-commercial use only</i>'
        )
        hdr.setWordWrap(True)
        lay.addWidget(hdr)

        txt = QTextEdit()
        txt.setReadOnly(True)
        txt.setHtml(_DISCLAIMER_HTML)
        txt.setMinimumHeight(380)
        lay.addWidget(txt)

        btn_row = QHBoxLayout()
        btn_row.addStretch()

        if accept_mode:
            chk = QCheckBox(
                '✅  Tôi đã đọc, hiểu và ĐỒNG Ý với tất cả các điều khoản trên\n'
                '     I have read, understood and AGREE to all the terms above'
            )
            lay.addWidget(chk)

            btn_decline = QPushButton('❌  Từ chối / Decline')
            btn_decline.setStyleSheet(
                'background:#B71C1C;color:white;font-weight:bold;'
                'padding:8px 20px;border-radius:4px')
            btn_accept = QPushButton('✅  Đồng ý & Tiếp tục / Accept & Continue')
            btn_accept.setEnabled(False)
            btn_accept.setStyleSheet(
                'background:#1B5E20;color:white;font-weight:bold;'
                'padding:8px 20px;border-radius:4px')
            btn_row.addWidget(btn_decline)
            btn_row.addWidget(btn_accept)
            chk.toggled.connect(btn_accept.setEnabled)
            btn_accept.clicked.connect(dlg.accept)
            btn_decline.clicked.connect(dlg.reject)
        else:
            btn_close = QPushButton('✕  Đóng / Close')
            btn_close.setStyleSheet(
                'background:#37474F;color:white;font-weight:bold;'
                'padding:8px 24px;border-radius:4px')
            btn_close.clicked.connect(dlg.accept)
            btn_row.addWidget(btn_close)

        lay.addLayout(btn_row)

        _QD_Accepted = getattr(
            __import__('qgis.PyQt.QtWidgets',
                       fromlist=['QDialog']).QDialog,
            'Accepted',
            getattr(
                getattr(__import__('qgis.PyQt.QtWidgets',
                                   fromlist=['QDialog']).QDialog,
                        'DialogCode', None), 'Accepted', 1)
        )

        result = dlg.exec() if hasattr(dlg, 'exec') else dlg.exec()
        return result == _QD_Accepted

    # ------------------------------------------------------------------
    # Silent satellite engine install
    # ------------------------------------------------------------------

    def _check_satellite_engine(self):
        """Silently install earthengine-api if missing (background)."""
        try:
            import ee  # noqa: F401
            return  # Already installed — nothing to do
        except ImportError:
            pass  # nosec B110

        # Deferred start: wait 5s so QGIS UI finishes loading first
        from qgis.PyQt.QtCore import QTimer
        QTimer.singleShot(5000, self._install_satellite_engine)

    def _install_satellite_engine(self):
        """Run pip install in a background thread using pip's Python API."""
        from qgis.PyQt.QtCore import QThread, pyqtSignal


        class _PipWorker(QThread):
            done = pyqtSignal(bool, str)

            def run(self):
                """Install earthengine-api using pip's Python API (no subprocess)."""
                try:
                    # Use pip's internal API — avoids subprocess B603/B607 warning.
                    # pip._internal.cli.main has been stable since pip 10 (2018).
                    from pip._internal.cli.main import main as _pip
                    ret = _pip([
                        "install", "earthengine-api",
                        "--quiet", "--disable-pip-version-check", "--user",
                    ])
                    if ret == 0:
                        self.done.emit(True, "")
                    else:
                        self.done.emit(False, f"pip exited with code {ret}")
                except ImportError:
                    # pip._internal not accessible — very old pip version
                    self.done.emit(
                        False,
                        "pip API unavailable. Run manually:\n"
                        "  python -m pip install earthengine-api --user"
                    )
                except Exception as e:
                    self.done.emit(False, str(e))


        self.iface.messageBar().pushInfo(
            "LVT4U",
            tr("Installing satellite image engine... (background)"))

        self._pip_worker = _PipWorker()
        self._pip_worker.done.connect(self._on_engine_installed)
        self._pip_worker.start()

    @staticmethod
    def _find_python_exe():
        """Find the correct python.exe for QGIS's Python environment."""
        import sys

        # Method 1: Check if sys.executable is already python.exe
        exe = sys.executable
        if os.path.basename(exe).lower().startswith("python"):
            return exe

        # Method 2: Look in QGIS's apps/Python3XX/ directory
        # sys.executable = .../QGIS 3.xx/bin/qgis-bin.exe
        # python.exe    = .../QGIS 3.xx/apps/Python3XX/python.exe
        qgis_root = os.path.dirname(os.path.dirname(exe))
        apps_dir = os.path.join(qgis_root, "apps")
        if os.path.isdir(apps_dir):
            for d in os.listdir(apps_dir):
                if d.lower().startswith("python3"):
                    candidate = os.path.join(apps_dir, d, "python.exe")
                    if os.path.isfile(candidate):
                        return candidate

        # Method 3: Use sys.prefix (Python's install prefix)
        candidate = os.path.join(sys.prefix, "python.exe")
        if os.path.isfile(candidate):
            return candidate

        # Method 4: bin/python.exe next to qgis-bin.exe
        candidate = os.path.join(os.path.dirname(exe), "python.exe")
        if os.path.isfile(candidate):
            return candidate

        return None

    def _on_engine_installed(self, success, error_msg):
        """Handle pip install result."""
        if success:
            self.iface.messageBar().pushSuccess(
                "LVT4U",
                tr("Satellite image engine installed successfully."))
        else:
            self.iface.messageBar().pushWarning(
                "LVT4U",
                f"Satellite engine install failed: {error_msg[:200]}")
        # Clean up worker
        try:
            self._pip_worker.deleteLater()
        except (RuntimeError, AttributeError):
            pass  # nosec B110 — QObject already deleted by Qt

    # ------------------------------------------------------------------
    # Utilities
    # ------------------------------------------------------------------

    def _install_style_library(self):
        """Import symbol libraries into QGIS Style Manager.

        Imports TCVN 11565-2016 library and any bundled XML files from
        layer_editor/styles/. Runs once per user profile.
        """
        from qgis.core import QgsStyle
        style = QgsStyle.defaultStyle()

        # --- 1. TCVN 11565-2016 (existing) ---
        tcvn_key = "LVT4U/style_imported_v1"
        if not QSettings().value(tcvn_key, False, type=bool):
            xml_path = os.path.join(
                self.plugin_dir, "thematic_map", "data",
                "tcvn11565_style.xml"
            )
            if os.path.isfile(xml_path):
                try:
                    count_before = style.symbolCount()
                    ok = style.importXml(xml_path)
                    if ok:
                        added = style.symbolCount() - count_before
                        QSettings().setValue(tcvn_key, True)
                        self.iface.messageBar().pushSuccess(
                            "LVT4U",
                            f"✅ TCVN 11565-2016: {added} symbols installed"
                        )
                except Exception as e:
                    self.iface.messageBar().pushWarning(
                        "LVT4U", f"⚠️ TCVN import error: {e}")

        # --- 2. LVT bundled XML libraries ---
        lvt_key = "LVT4U/style_imported_lvt_v1"
        if not QSettings().value(lvt_key, False, type=bool):
            styles_dir = os.path.join(
                self.plugin_dir, "layer_editor", "styles")
            if os.path.isdir(styles_dir):
                total_added = 0
                for fname in os.listdir(styles_dir):
                    if not fname.lower().endswith('.xml'):
                        continue
                    fpath = os.path.join(styles_dir, fname)
                    try:
                        before = style.symbolCount()
                        style.importXml(fpath)
                        total_added += style.symbolCount() - before
                    except Exception:  # nosec B110
                        pass  # nosec B110
                if total_added > 0:
                    QSettings().setValue(lvt_key, True)
                    self.iface.messageBar().pushSuccess(
                        "LVT4U",
                        f"✅ LVT Style Library: {total_added} symbols installed"
                    )

    def _show_placeholder(self, module_name):
        """Display a message bar notification for modules not yet implemented.

        Args:
            module_name: Human-readable name of the module.
        """
        self.iface.messageBar().pushInfo(
            "LVT4U",
            tr("{module} — Coming soon!").format(module=module_name),
        )

    def _add_xyz_basemap(self, name, url, zmin=0, zmax=19):
        """Add an XYZ tile layer to the current QGIS project.

        Uses the standard XYZ tile URI format supported by QGIS raster provider.

        Args:
            name: Display name for the layer.
            url: XYZ tile URL template with {x}, {y}, {z} placeholders.
            zmin: Minimum zoom level (default 0).
            zmax: Maximum zoom level (default 19).
        """
        from qgis.core import QgsRasterLayer, QgsProject
        from urllib.parse import quote as _url_quote

        # URL-encode the tile URL so that '&' inside it won't clash
        # with the '&' separating QGIS URI parameters.
        encoded_url = _url_quote(url, safe=':/{}.?%')

        uri = (
            f"type=xyz&url={encoded_url}"
            f"&zmin={zmin}&zmax={zmax}"
        )

        # Avoid duplicates — check if a layer with this name already exists
        existing = QgsProject.instance().mapLayersByName(name)
        if existing:
            self.iface.messageBar().pushInfo(
                "LVT4U",
                tr("{name} is already loaded.").format(name=name),
            )
            return

        layer = QgsRasterLayer(uri, name, "wms")
        if layer.isValid():
            QgsProject.instance().addMapLayer(layer)
            self.iface.messageBar().pushSuccess(
                "LVT4U",
                tr("{name} added successfully.").format(name=name),
            )
        else:
            self.iface.messageBar().pushWarning(
                "LVT4U",
                tr("Failed to load {name}.").format(name=name),
            )
