# -*- coding: utf-8 -*-
"""
LVT4U CAD → SHP Converter — Convert DWG/DXF/DGN files to Shapefile.

Core flow:
    1. Select DWG/DXF/DGN file
    2. Set coordinate system (VN-2000 / Custom / WGS84)
    3. Scan file to discover CAD layers
    4. Select layers to convert
    5. Set output folder and options
    6. Convert selected layers to SHP

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

import os
import tempfile

from qgis.PyQt.QtCore import Qt
from ..shared.qt_compat import QtCompat, MsgButton
from qgis.PyQt.QtWidgets import (
    QDialog, QVBoxLayout, QHBoxLayout, QLabel,
    QComboBox, QPushButton, QFileDialog, QMessageBox, QGroupBox,
    QProgressBar, QApplication, QCheckBox, QLineEdit,
    QRadioButton, QHeaderView, QTableWidget, QTableWidgetItem,
    QAbstractItemView,
)
from qgis.PyQt.QtGui import QColor, QBrush
from qgis.core import (
    QgsProject, QgsVectorLayer, QgsVectorFileWriter,
    QgsCoordinateReferenceSystem, QgsCoordinateTransform,
    QgsCoordinateTransformContext,
)
from qgis.gui import QgsProjectionSelectionWidget

from ..shared.i18n import current_language
from ..shared.help_dialog import HelpDialog
from ..shared.text_utils import remove_accents
from ..shared.encoding_vn import detect_encoding, convert_text
from ._cad_scan import (
    scan_sublayers, GEOM_POINT, GEOM_LINE,
    GEOM_POLYGON, GEOM_TEXT,
)

# ── Bilingual labels ──────────────────────────────────────────────────
_VI = {
    'DWG/DXF/DGN → SHP Converter': 'Chuyển đổi CAD → SHP',
    'Source File': 'File nguồn',
    'Browse...': 'Chọn file...',
    'Supported: .dwg .dxf .dgn': 'Hỗ trợ: .dwg .dxf .dgn',
    'Coordinate System': 'Hệ tọa độ',
    'VN-2000': 'VN-2000',
    'Custom CRS': 'Tùy chỉnh',
    'Undefined (WGS84)': 'Không xác định (WGS84)',
    'Scan File': 'Quét file',
    'CAD Layers': 'Danh sách lớp CAD',
    'Select All': 'Chọn tất cả',
    'Deselect All': 'Bỏ tất cả',
    'Layer Name': 'Tên lớp',
    'Type': 'Kiểu',
    'Features': 'Số ĐT',
    'Options': 'Tùy chọn',
    'Expand block references': 'Phân tách Block thành đối tượng đơn',
    'Add to map after conversion': 'Thêm vào bản đồ sau khi chuyển đổi',
    'Separate SHP per geometry type': 'Tách SHP theo kiểu hình học',
    'Output Folder': 'Thư mục xuất',
    'Browse Folder...': 'Chọn thư mục...',
    'Convert': 'Chuyển đổi',
    'Close': 'Đóng',
    'Scanning file...': 'Đang quét file...',
    'Converting...': 'Đang chuyển đổi...',
    'No layers selected': 'Chưa chọn lớp nào',
    'Select source file first': 'Chọn file nguồn trước',
    'Select output folder': 'Chọn thư mục xuất',
    'Conversion complete': 'Chuyển đổi hoàn tất',
    'layers converted': 'lớp đã chuyển đổi',
    'features total': 'đối tượng',
    'No file selected': 'Chưa chọn file',
    'Error': 'Lỗi',
    'Processing...': 'Đang xử lý...',
    'No layers found': 'Không tìm thấy lớp nào trong file',
    'Guide': 'Hướng dẫn',
    # New Phase 2 labels
    'Scan & Preview': 'Quét & Xem trước',
    'Level/Layer': 'Level/Layer',
    'Point': 'Điểm',
    'Line': 'Đường',
    'Polygon': 'Vùng',
    'Text': 'Chữ',
    'Color': 'Màu',
    'Encoding': 'Mã hóa',
    'Source encoding': 'Encoding nguồn',
    'Auto detect': 'Tự động',
    'Text preview': 'Xem trước chữ',
    'Raw': 'Gốc',
    'Converted': 'Đã chuyển',
    'Extract text layer': 'Tách Text thành lớp riêng',
    'Convert encoding': 'Chuyển đổi TCVN3/VNI → Unicode',
    'CRS assigned': 'Hệ tọa độ đã chọn',
    'Search province...': 'Tìm tỉnh thành...',
    'levels': 'levels',
}

# Geometry type mapping for display
_GEOM_MAP = {
    'Point': 'Point', 'LineString': 'Line', 'Polygon': 'Polygon',
    'MultiPoint': 'Point', 'MultiLineString': 'Line',
    'MultiPolygon': 'Polygon', 'CompoundCurve': 'Line',
    'CircularString': 'Line', 'CurvePolygon': 'Polygon',
    'Unknown': 'Unknown', 'No Geometry': 'None',
    'GeometryCollection': 'Mixed',
}


class DwgConverterDialog(QDialog):
    """Dialog for converting DWG/DXF/DGN files to ESRI Shapefile."""

    def __init__(self, iface, parent=None):
        super().__init__(parent or iface.mainWindow())
        self.iface = iface
        self.lang = current_language()
        self._temp_gpkg = None
        self._level_stats: dict = {}  # level_name → LevelStat

        self.setMinimumSize(820, 680)
        self.setWindowFlags(
            self.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint
        )

        self._setup_ui()
        self._refresh_ui_text()

    # ------------------------------------------------------------------
    # Translation helper
    # ------------------------------------------------------------------

    def _t(self, text):
        """Translate text based on current language."""
        return _VI.get(text, text) if self.lang == 'vi' else text

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

    def _setup_ui(self):
        """Build the dialog UI."""
        main_ly = QVBoxLayout(self)
        main_ly.setSpacing(8)

        # ── Language toggle (top-right) ──
        top_row = QHBoxLayout()
        top_row.addStretch()
        self.btn_lang = QPushButton("🇬🇧 English")
        self.btn_lang.setFixedWidth(120)
        self.btn_lang.clicked.connect(self._toggle_lang)
        top_row.addWidget(self.btn_lang)
        main_ly.addLayout(top_row)

        # ── Source file ──
        src_row = QHBoxLayout()
        self.lbl_source = QLabel("📁 Source File:")
        self.txt_input = QLineEdit()
        self.txt_input.setReadOnly(True)
        self.txt_input.setPlaceholderText("...")
        self.btn_browse_input = QPushButton("Browse...")
        self.btn_browse_input.clicked.connect(self._browse_input)
        src_row.addWidget(self.lbl_source)
        src_row.addWidget(self.txt_input, 1)
        src_row.addWidget(self.btn_browse_input)
        main_ly.addLayout(src_row)

        self.lbl_support = QLabel("Supported: .dwg .dxf .dgn")
        self.lbl_support.setStyleSheet("color: #888; font-size: 11px;")
        main_ly.addWidget(self.lbl_support)

        # ── CRS Group ──
        self.grp_crs = self._build_crs_group()
        main_ly.addWidget(self.grp_crs)

        # ── Scan button ──
        scan_row = QHBoxLayout()
        self.btn_scan = QPushButton("🔍 Scan File")
        self.btn_scan.setFixedHeight(34)
        self.btn_scan.setStyleSheet(
            "QPushButton{background:#1565c0;color:#fff;font-weight:bold;"
            "padding:4px 14px;border-radius:4px}"
            "QPushButton:hover{background:#1976d2}"
        )
        self.btn_scan.clicked.connect(self._scan_file)
        scan_row.addWidget(self.btn_scan)
        scan_row.addStretch()
        main_ly.addLayout(scan_row)

        # ── CAD Layers ──
        self.grp_layers = QGroupBox("CAD Layers")
        layers_ly = QVBoxLayout(self.grp_layers)

        # Select/Deselect buttons
        sel_row = QHBoxLayout()
        self.btn_select_all = QPushButton("Select All")
        self.btn_select_all.setFixedHeight(28)
        self.btn_select_all.clicked.connect(self._select_all)
        self.btn_deselect_all = QPushButton("Deselect All")
        self.btn_deselect_all.setFixedHeight(28)
        self.btn_deselect_all.clicked.connect(self._deselect_all)
        sel_row.addWidget(self.btn_select_all)
        sel_row.addWidget(self.btn_deselect_all)
        sel_row.addStretch()
        self.lbl_summary = QLabel("")
        self.lbl_summary.setStyleSheet("color: #555; font-style: italic;")
        sel_row.addWidget(self.lbl_summary)
        layers_ly.addLayout(sel_row)

        # ── Level/Layer statistics table (7 columns) ──────────────────────
        # Cols: Checkbox | Level/Layer | Điểm | Đường | Vùng | Chữ | Màu
        self.tbl_layers = QTableWidget(0, 7)
        self.tbl_layers.setHorizontalHeaderLabels(
            ['☑', 'Level/Layer', 'Điểm', 'Đường', 'Vùng', 'Chữ', 'Màu']
        )
        self.tbl_layers.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
        self.tbl_layers.setAlternatingRowColors(True)
        self.tbl_layers.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
        self.tbl_layers.verticalHeader().setVisible(False)
        self.tbl_layers.setMinimumHeight(180)
        self.tbl_layers.setMinimumWidth(560)
        hdr = self.tbl_layers.horizontalHeader()
        hdr.setDefaultAlignment(QtCompat.AlignCenter)
        hdr.resizeSection(0, 30)                         # checkbox
        hdr.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)  # layer name
        hdr.resizeSection(2, 52)                         # point
        hdr.resizeSection(3, 56)                         # line
        hdr.resizeSection(4, 52)                         # polygon
        hdr.resizeSection(5, 42)                         # text
        hdr.resizeSection(6, 54)                         # color
        self.tbl_layers.itemChanged.connect(self._on_table_item_changed)
        self.tbl_layers.currentCellChanged.connect(self._on_layer_selected)
        layers_ly.addWidget(self.tbl_layers)

        # ── Encoding options ────────────────────────────────────────────────
        enc_row = QHBoxLayout()
        self.lbl_encoding = QLabel('Encoding nguồn:')
        self.cmb_encoding = QComboBox()
        for enc_label in [
            '🔍 Tự động (Auto)', 'UNICODE (không chuyển)', 'TCVN3', 'VNI', 'VIQR'
        ]:
            self.cmb_encoding.addItem(enc_label)
        self.cmb_encoding.currentIndexChanged.connect(self._on_encoding_changed)
        enc_row.addWidget(self.lbl_encoding)
        enc_row.addWidget(self.cmb_encoding)
        enc_row.addStretch()
        layers_ly.addLayout(enc_row)

        # ── Text preview panel ──────────────────────────────────────────────
        self.grp_preview = QGroupBox('🔤 Xem trước chữ (click vào level để xem)')
        self.grp_preview.setCheckable(True)
        self.grp_preview.setChecked(False)  # collapsed by default
        prev_ly = QVBoxLayout(self.grp_preview)
        prev_row = QHBoxLayout()
        self.lbl_raw = QLabel('Gốc:')
        self.txt_raw = QLineEdit()
        self.txt_raw.setReadOnly(True)
        self.txt_raw.setPlaceholderText('(chọn 1 level có chữ)')
        self.lbl_converted = QLabel('Đã chuyển:')
        self.txt_converted = QLineEdit()
        self.txt_converted.setReadOnly(True)
        self.txt_converted.setStyleSheet(
            'color: #1b5e20; font-weight: bold;')
        prev_row.addWidget(self.lbl_raw)
        prev_row.addWidget(self.txt_raw, 1)
        prev_row.addWidget(self.lbl_converted)
        prev_row.addWidget(self.txt_converted, 1)
        prev_ly.addLayout(prev_row)
        layers_ly.addWidget(self.grp_preview)
        main_ly.addWidget(self.grp_layers)

        # ── Options ──
        self.grp_options = QGroupBox("Options")
        opt_ly = QVBoxLayout(self.grp_options)
        self.chk_expand_blocks = QCheckBox("Expand block references")
        self.chk_expand_blocks.setChecked(True)
        self.chk_add_to_map = QCheckBox("Add to map after conversion")
        self.chk_add_to_map.setChecked(True)
        self.chk_separate_geom = QCheckBox(
            "Separate SHP per geometry type")
        self.chk_separate_geom.setChecked(True)
        self.chk_extract_text = QCheckBox(
            'Tách Text entities thành lớp SHP riêng (+ TEXTSTRING field)')
        self.chk_extract_text.setChecked(True)
        self.chk_convert_encoding = QCheckBox(
            'Chuyển đổi TCVN3/VNI → Unicode (áp dụng cho text field)')
        self.chk_convert_encoding.setChecked(True)
        opt_ly.addWidget(self.chk_expand_blocks)
        opt_ly.addWidget(self.chk_add_to_map)
        opt_ly.addWidget(self.chk_separate_geom)
        opt_ly.addWidget(self.chk_extract_text)
        opt_ly.addWidget(self.chk_convert_encoding)
        main_ly.addWidget(self.grp_options)

        # ── Output folder ──
        out_row = QHBoxLayout()
        self.lbl_output = QLabel("📂 Output Folder:")
        self.txt_output = QLineEdit()
        self.txt_output.setReadOnly(True)
        self.txt_output.setPlaceholderText("...")
        self.btn_browse_output = QPushButton("Browse Folder...")
        self.btn_browse_output.clicked.connect(self._browse_output)
        out_row.addWidget(self.lbl_output)
        out_row.addWidget(self.txt_output, 1)
        out_row.addWidget(self.btn_browse_output)
        main_ly.addLayout(out_row)

        # ── Progress bar ──
        self.progress = QProgressBar()
        self.progress.setVisible(False)
        self.progress.setTextVisible(True)
        main_ly.addWidget(self.progress)

        # ── Status label ──
        self.lbl_status = QLabel("")
        self.lbl_status.setStyleSheet("color: #555;")
        main_ly.addWidget(self.lbl_status)

        # ── Bottom buttons ──
        btn_row = QHBoxLayout()
        self.btn_help = QPushButton("❓ Guide")
        self.btn_help.setFixedHeight(36)
        self.btn_help.setStyleSheet(
            "QPushButton{background:#ff8f00;color:#fff;font-weight:bold;"
            "padding:4px 14px;border-radius:4px}"
            "QPushButton:hover{background:#ffa000}"
        )
        self.btn_help.clicked.connect(self._show_help)
        btn_row.addWidget(self.btn_help)

        btn_row.addStretch()

        self.btn_convert = QPushButton("🔄 Convert")
        self.btn_convert.setFixedHeight(36)
        self.btn_convert.setStyleSheet(
            "QPushButton{background:#2e7d32;color:#fff;font-weight:bold;"
            "padding:4px 20px;border-radius:4px}"
            "QPushButton:hover{background:#388e3c}"
        )
        self.btn_convert.clicked.connect(self._convert)
        btn_row.addWidget(self.btn_convert)

        self.btn_close = QPushButton("Close")
        self.btn_close.setFixedHeight(36)
        self.btn_close.clicked.connect(self.close)
        btn_row.addWidget(self.btn_close)

        main_ly.addLayout(btn_row)

    # ------------------------------------------------------------------
    # CRS Group Builder
    # ------------------------------------------------------------------

    def _build_crs_group(self):
        """Build CRS selection UI with VN-2000 zones and search filter."""
        group = QGroupBox("Coordinate System")
        layout = QVBoxLayout(group)

        # Radio buttons
        self.radio_vn2000 = QRadioButton("VN-2000")
        self.radio_vn2000.setChecked(True)
        self.radio_custom = QRadioButton("Custom CRS")
        self.radio_wgs84 = QRadioButton("Undefined (WGS84)")

        # Search filter for VN-2000 provinces
        self.txt_crs_search = QLineEdit()
        self.txt_crs_search.setPlaceholderText('🔍 Tìm tỉnh thành...')
        self.txt_crs_search.textChanged.connect(self._filter_crs_combo)

        # VN-2000 zone combo — load from root _crs_list
        from .._crs_list import CRS_LIST
        self._crs_list_full = CRS_LIST  # store for search filtering
        self.cmb_vn2000 = QComboBox()
        for label, epsg in CRS_LIST:
            if not epsg:
                # Section header — add as disabled separator
                self.cmb_vn2000.addItem(f"── {label} ──", "")
                idx = self.cmb_vn2000.count() - 1
                model = self.cmb_vn2000.model()
                item = model.item(idx)
                if item:
                    item.setEnabled(False)
            else:
                self.cmb_vn2000.addItem(label, epsg)
        # Default to first valid entry
        for i in range(self.cmb_vn2000.count()):
            if self.cmb_vn2000.itemData(i):
                self.cmb_vn2000.setCurrentIndex(i)
                break
        self.cmb_vn2000.currentIndexChanged.connect(
            lambda: self._update_crs_resolved_label())

        # Resolved CRS label
        self.lbl_crs_resolved = QLabel('')
        self.lbl_crs_resolved.setStyleSheet(
            'color: #1b5e20; font-size: 11px; font-style: italic;')

        # Custom CRS widget
        self.crs_widget = QgsProjectionSelectionWidget()
        self.crs_widget.setCrs(
            QgsCoordinateReferenceSystem("EPSG:4326"))
        self.crs_widget.setEnabled(False)

        # Row 0: Province search
        layout.addWidget(self.txt_crs_search)

        # Row 1: VN-2000 radio + combo
        h1 = QHBoxLayout()
        h1.addWidget(self.radio_vn2000)
        h1.addWidget(self.cmb_vn2000, 1)
        layout.addLayout(h1)

        # Resolved CRS hint
        layout.addWidget(self.lbl_crs_resolved)

        # Row 2: Custom radio + CRS widget
        h2 = QHBoxLayout()
        h2.addWidget(self.radio_custom)
        h2.addWidget(self.crs_widget, 1)
        layout.addLayout(h2)

        # Row 3: WGS84 radio
        layout.addWidget(self.radio_wgs84)

        # Signals — enable/disable associated widgets
        self.radio_vn2000.toggled.connect(
            lambda on: self.cmb_vn2000.setEnabled(on))
        self.radio_vn2000.toggled.connect(
            lambda on: self.txt_crs_search.setEnabled(on))
        self.radio_custom.toggled.connect(
            lambda on: self.crs_widget.setEnabled(on))

        self._update_crs_resolved_label()
        return group

    # ------------------------------------------------------------------
    # CRS Selection
    # ------------------------------------------------------------------

    def _get_selected_crs(self):
        """Get the currently selected CRS."""
        if self.radio_vn2000.isChecked():
            epsg = self.cmb_vn2000.currentData()
            if epsg:
                return QgsCoordinateReferenceSystem(epsg)
            return QgsCoordinateReferenceSystem("EPSG:4326")
        elif self.radio_custom.isChecked():
            return self.crs_widget.crs()
        else:
            return QgsCoordinateReferenceSystem("EPSG:4326")

    def _filter_crs_combo(self, text: str):
        """Filter VN-2000 province combo by search text (accent-insensitive)."""
        query = remove_accents(text.lower().strip())
        self.cmb_vn2000.blockSignals(True)
        self.cmb_vn2000.clear()
        crs_list = getattr(self, '_crs_list_full', [])
        if not query:
            # Restore full list
            for label, epsg in crs_list:
                if not epsg:
                    self.cmb_vn2000.addItem(f'── {label} ──', '')
                    idx = self.cmb_vn2000.count() - 1
                    item = self.cmb_vn2000.model().item(idx)
                    if item:
                        item.setEnabled(False)
                else:
                    self.cmb_vn2000.addItem(label, epsg)
        else:
            for label, epsg in crs_list:
                if not epsg:
                    continue
                if query in remove_accents(label.lower()):
                    self.cmb_vn2000.addItem(label, epsg)
        self.cmb_vn2000.blockSignals(False)
        self._update_crs_resolved_label()

    def _update_crs_resolved_label(self):
        """Update the resolved CRS label based on current selection."""
        epsg = self.cmb_vn2000.currentData()
        if epsg and self.radio_vn2000.isChecked():
            self.lbl_crs_resolved.setText(
                f'✅ {epsg} — {self.cmb_vn2000.currentText()}')
        else:
            self.lbl_crs_resolved.setText('')

    # ------------------------------------------------------------------
    # Language
    # ------------------------------------------------------------------

    def _toggle_lang(self):
        """Toggle between Vietnamese and English."""
        self.lang = 'en' if self.lang == 'vi' else 'vi'
        self._refresh_ui_text()

    def _refresh_ui_text(self):
        """Update all widget texts for current language."""
        t = self._t
        self.setWindowTitle(f"LVT4U — {t('DWG/DXF/DGN → SHP Converter')}")
        self.btn_lang.setText(
            "🇬🇧 English" if self.lang == 'en' else "🇻🇳 Tiếng Việt"
        )

        # Source file
        self.lbl_source.setText(f"📁 {t('Source File')}:")
        self.btn_browse_input.setText(t('Browse...'))
        self.lbl_support.setText(t('Supported: .dwg .dxf .dgn'))

        # CRS group
        self.grp_crs.setTitle(t('Coordinate System'))
        self.radio_vn2000.setText(t('VN-2000'))
        self.radio_custom.setText(t('Custom CRS'))
        self.radio_wgs84.setText(t('Undefined (WGS84)'))
        self.txt_crs_search.setPlaceholderText(
            f"🔍 {t('Search province...')}")

        # Scan button
        self.btn_scan.setText(f"🔍 {t('Scan File')}")

        # Layers group
        self.grp_layers.setTitle(t('CAD Layers'))
        self.btn_select_all.setText(t('Select All'))
        self.btn_deselect_all.setText(t('Deselect All'))
        self.tbl_layers.setHorizontalHeaderLabels([
            '☑',
            t('Level/Layer'),
            t('Point'),
            t('Line'),
            t('Polygon'),
            t('Text'),
            t('Color'),
        ])
        self.lbl_encoding.setText(f"{t('Source encoding')}:")

        # Options group
        self.grp_options.setTitle(t('Options'))
        self.chk_expand_blocks.setText(t('Expand block references'))
        self.chk_add_to_map.setText(t('Add to map after conversion'))
        self.chk_separate_geom.setText(
            t('Separate SHP per geometry type'))
        self.chk_extract_text.setText(t('Extract text layer'))
        self.chk_convert_encoding.setText(t('Convert encoding'))

        # Output folder
        self.lbl_output.setText(f"📂 {t('Output Folder')}:")
        self.btn_browse_output.setText(t('Browse Folder...'))

        # Bottom buttons
        self.btn_convert.setText(f"🔄 {t('Convert')}")
        self.btn_close.setText(t('Close'))
        self.btn_help.setText(f"❓ {t('Guide')}")

    # ------------------------------------------------------------------
    # File Browsing
    # ------------------------------------------------------------------

    def _browse_input(self):
        """Browse for DWG/DXF file."""
        path, _ = QFileDialog.getOpenFileName(
            self, 'Select CAD File', '',
            'CAD Files (*.dwg *.dxf *.dgn *.gpkg);;DWG (*.dwg);;DXF (*.dxf);;DGN (*.dgn);;GeoPackage (*.gpkg)'
        )
        if path:
            self.txt_input.setText(path)
            # Auto-set output folder to same directory
            if not self.txt_output.text().strip():
                self.txt_output.setText(os.path.dirname(path))

    def _browse_output(self):
        """Browse for output folder."""
        folder = QFileDialog.getExistingDirectory(
            self, 'Select Output Folder')
        if folder:
            self.txt_output.setText(folder)

    def _open_qgis_dwg_import(self):
        """Open QGIS's built-in DWG/DXF Import dialog."""
        try:
            # Find the Import DWG/DXF action in QGIS menus
            main_window = self.iface.mainWindow()
            for action in main_window.findChildren(
                    type(main_window.menuBar().actions()[0])):
                if 'dwg' in action.text().lower() or \
                   'dxf' in action.text().lower():
                    action.trigger()
                    return

            # Fallback: try the Project menu directly
            project_menu = self.iface.projectMenu()
            if project_menu:
                for action in project_menu.actions():
                    txt = action.text().lower()
                    if 'import' in txt and ('dwg' in txt or 'dxf' in txt):
                        action.trigger()
                        return
                    # Check submenus
                    if action.menu():
                        for sub_action in action.menu().actions():
                            txt2 = sub_action.text().lower()
                            if 'dwg' in txt2 or 'dxf' in txt2:
                                sub_action.trigger()
                                return

            # Last resort: try Import/Export submenu
            for action in main_window.menuBar().actions():
                if action.menu():
                    for sub in action.menu().actions():
                        if sub.menu():
                            for sub2 in sub.menu().actions():
                                txt = sub2.text().lower()
                                if 'dwg' in txt or 'dxf' in txt:
                                    sub2.trigger()
                                    return

            QMessageBox.information(
                self, 'LVT4U',
                'Vào menu: Project → Import/Export → '
                'Import Layers from DWG/DXF'
                if self.lang == 'vi' else
                'Go to: Project → Import/Export → '
                'Import Layers from DWG/DXF')
        except Exception:
            QMessageBox.information(
                self, 'LVT4U',
                'Vào menu: Project → Import/Export → '
                'Import Layers from DWG/DXF'
                if self.lang == 'vi' else
                'Go to: Project → Import/Export → '
                'Import Layers from DWG/DXF')

    # ------------------------------------------------------------------
    # Layer Selection
    # ------------------------------------------------------------------

    def _select_all(self):
        """Check all rows in the layer table."""
        self.tbl_layers.blockSignals(True)
        for i in range(self.tbl_layers.rowCount()):
            item = self.tbl_layers.item(i, 0)
            if item:
                item.setCheckState(QtCompat.Checked)
        self.tbl_layers.blockSignals(False)
        self._update_layer_summary()

    def _deselect_all(self):
        """Uncheck all rows in the layer table."""
        self.tbl_layers.blockSignals(True)
        for i in range(self.tbl_layers.rowCount()):
            item = self.tbl_layers.item(i, 0)
            if item:
                item.setCheckState(QtCompat.Unchecked)
        self.tbl_layers.blockSignals(False)
        self._update_layer_summary()

    def _on_table_item_changed(self, item):
        """Handle checkbox change in table."""
        if item.column() == 0:
            self._update_layer_summary()

    def _on_layer_selected(self, row, col, prev_row, prev_col):
        """Show encoding preview for the selected level."""
        if row < 0:
            return
        chk_item = self.tbl_layers.item(row, 0)
        if not chk_item:
            return
        level_name = chk_item.data(QtCompat.UserRole)
        if not self._level_stats or level_name not in self._level_stats:
            return
        stat = self._level_stats[level_name]
        if stat.text_samples:
            raw = stat.text_samples[0]
            self.txt_raw.setText(raw)
            enc = self._get_current_encoding()
            converted = convert_text(raw, enc)
            self.txt_converted.setText(converted)
        else:
            self.txt_raw.setText('(level này không có chữ)')
            self.txt_converted.setText('')

    def _on_encoding_changed(self, index):
        """Refresh text preview when encoding selection changes."""
        row = self.tbl_layers.currentRow()
        if row >= 0:
            self._on_layer_selected(row, 0, -1, -1)

    def _get_current_encoding(self) -> str:
        """Get the encoding string from the combo box."""
        enc_text = self.cmb_encoding.currentText()
        if 'TCVN3' in enc_text:
            return 'TCVN3'
        if 'VNI' in enc_text:
            return 'VNI'
        if 'VIQR' in enc_text:
            return 'VIQR'
        return 'UNICODE'

    def _update_layer_summary(self):
        """Update the summary label with selected/total count."""
        total = self.tbl_layers.rowCount()
        checked = 0
        total_features = 0
        for i in range(total):
            chk = self.tbl_layers.item(i, 0)
            if chk and chk.checkState() == QtCompat.Checked:
                checked += 1
                level_name = chk.data(QtCompat.UserRole)
                if self._level_stats and level_name in self._level_stats:
                    total_features += self._level_stats[level_name].total
        self.lbl_summary.setText(
            f"{checked}/{total} {self._t('levels')} — "
            f"{total_features} {self._t('features total')}"
        )

    # ------------------------------------------------------------------
    # Scan File
    # ------------------------------------------------------------------

    def _scan_file(self):
        """Scan DWG/DXF and populate layer list.

        Strategy for DXF:
            Direct OGR loading → sublayers → populate tree

        Strategy for DWG:
            1. Try direct OGR (works for DWG R2000 only)
            2. Try QGIS internal DWG import → GeoPackage → read layers
            3. Guide user to use QGIS Import DWG dialog first
        """
        input_path = self.txt_input.text().strip()
        if not input_path or not os.path.exists(input_path):
            QMessageBox.warning(
                self, 'LVT4U', self._t('No file selected'))
            return

        self.progress.setVisible(True)
        self.progress.setValue(10)
        self.progress.setFormat(self._t('Scanning file...'))
        self.lbl_status.setText(self._t('Scanning file...'))
        QApplication.processEvents()

        try:
            crs = self._get_selected_crs()
            is_dwg = input_path.lower().endswith('.dwg')
            is_dxf = input_path.lower().endswith('.dxf')

            # Prepare temp GeoPackage path
            self._temp_gpkg = os.path.join(
                tempfile.gettempdir(), 'lvt_dwg_import.gpkg')
            for ext in ['', '-wal', '-shm']:
                p = self._temp_gpkg + ext
                if os.path.exists(p):
                    try:
                        os.remove(p)
                    except OSError:
                        pass  # nosec B110

            scan_ok = False
            is_gpkg = input_path.lower().endswith('.gpkg')
            is_dgn = input_path.lower().endswith('.dgn')

            if is_gpkg:
                # ── GPKG: Direct loading (from previous QGIS DWG import) ──
                self._temp_gpkg = input_path
                self._populate_layer_list_from_gpkg(input_path)
                scan_ok = self.tbl_layers.rowCount() > 0
            elif is_dxf or is_dgn:
                # ── DXF/DGN: Direct OGR loading (always works) ──
                scan_ok = self._scan_dxf_direct(input_path)
            elif is_dwg:
                # ── DWG: Multi-method approach ──
                scan_ok = self._scan_dwg(input_path, crs)

            if not scan_ok:
                self._show_dwg_help_dialog(input_path, is_dwg)

        except Exception as e:
            QMessageBox.critical(
                self, 'LVT4U', f'{self._t("Error")}: {str(e)}')
        finally:
            self.progress.setVisible(False)
            self.lbl_status.setText("")

    def _scan_dxf_direct(self, dxf_path):
        """Scan DXF/DGN file using the LevelStat-based scanner."""
        self.progress.setFormat(self._t('Scanning file...') + ' (OGR scan)')
        self.progress.setValue(30)
        QApplication.processEvents()

        stats = scan_sublayers(dxf_path)
        if not stats:
            # Fallback: try the old sublayer-listing approach
            return self._scan_dxf_legacy(dxf_path)

        self._populate_table_from_stats(stats, source='ogr',
                                        input_path=dxf_path)
        self.progress.setValue(100)
        QApplication.processEvents()
        return self.tbl_layers.rowCount() > 0

    def _scan_dxf_legacy(self, dxf_path):
        """Fallback: populate table from OGR sublayer metadata only."""
        vlayer = QgsVectorLayer(dxf_path, 'scan', 'ogr')
        if not vlayer.isValid():
            vlayer = QgsVectorLayer(
                f'{dxf_path}|option:DXF_ENCODING=UTF-8', 'scan', 'ogr')
        if not vlayer.isValid():
            return False

        sub_layers = vlayer.dataProvider().subLayers()
        del vlayer
        if not sub_layers:
            return False

        # Build minimal LevelStat dict from sublayer metadata
        from ._cad_scan import LevelStat, GEOM_POINT, GEOM_LINE, GEOM_POLYGON, GEOM_NONE
        stats: dict = {}
        for sub in sub_layers:
            parts = sub.split('!!::!!')
            if len(parts) < 4:
                continue
            layer_name = parts[1]
            try:
                count = int(parts[2])
            except ValueError:
                count = 0
            geom_str = parts[3] if len(parts) > 3 else 'Unknown'

            stat = LevelStat(name=layer_name)
            # Map OGR geom type string to kind
            kind_map = {
                'Point': GEOM_POINT, 'MultiPoint': GEOM_POINT,
                'LineString': GEOM_LINE, 'MultiLineString': GEOM_LINE,
                'CompoundCurve': GEOM_LINE, 'CircularString': GEOM_LINE,
                'Polygon': GEOM_POLYGON, 'MultiPolygon': GEOM_POLYGON,
                'CurvePolygon': GEOM_POLYGON,
            }
            kind = kind_map.get(geom_str, GEOM_NONE)
            stat.counts[kind] = count
            stat._source_geom = geom_str  # store for export
            stat._input_path = dxf_path
            stats[layer_name] = stat

        self._populate_table_from_stats(
            stats, source='ogr', input_path=dxf_path)
        self.progress.setValue(100)
        QApplication.processEvents()
        return self.tbl_layers.rowCount() > 0

    def _scan_dwg(self, dwg_path, crs):
        """Scan DWG file using multiple strategies."""
        errors = []

        # --- Method 1: Try QGIS DWG Importer (C++ internal) ---
        self.progress.setFormat(
            self._t('Scanning file...') + ' (QGIS Importer)')
        self.progress.setValue(20)
        QApplication.processEvents()

        try:
            ok = self._try_qgis_dwg_importer(dwg_path, crs)
            if ok:
                return True
        except Exception as e:
            errors.append(f'QgsDwgImporter: {e}')

        # --- Method 2: Try Processing algorithms ---
        self.progress.setFormat(
            self._t('Scanning file...') + ' (Processing)')
        self.progress.setValue(40)
        QApplication.processEvents()

        try:
            ok = self._try_processing_import(dwg_path, crs)
            if ok:
                return True
        except Exception as e:
            errors.append(f'Processing: {e}')

        # --- Method 3: Try direct OGR (only DWG R2000) ---
        self.progress.setFormat(
            self._t('Scanning file...') + ' (OGR/CAD)')
        self.progress.setValue(60)
        QApplication.processEvents()

        try:
            ok = self._scan_dxf_direct(dwg_path)  # OGR can try CAD driver
            if ok:
                return True
        except Exception as e:
            errors.append(f'OGR: {e}')

        # --- Method 4: Try loading from existing GeoPackage ---
        # (user may have already imported via QGIS menu)
        self.progress.setFormat(
            self._t('Scanning file...') + ' (GPKG)')
        self.progress.setValue(80)
        QApplication.processEvents()

        try:
            ok = self._try_load_existing_gpkg(dwg_path)
            if ok:
                return True
        except Exception as e:
            errors.append(f'GPKG: {e}')

        self._last_errors = errors
        return False

    def _try_qgis_dwg_importer(self, dwg_path, crs):
        """Try QGIS's internal QgsDwgImporter (available in some builds)."""
        # Try to import QgsDwgImporter from various locations
        QgsDwgImporter = None
        for module_path in [
            'qgis.core', 'qgis._core', 'qgis.app', 'qgis._app',
        ]:
            try:
                mod = __import__(module_path, fromlist=['QgsDwgImporter'])
                QgsDwgImporter = getattr(mod, 'QgsDwgImporter', None)
                if QgsDwgImporter:
                    break
            except (ImportError, AttributeError):
                continue

        if QgsDwgImporter is None:
            raise ImportError('QgsDwgImporter not available in Python bindings')

        crs_obj = crs if crs and crs.isValid() else \
            QgsCoordinateReferenceSystem('EPSG:4326')

        importer = QgsDwgImporter(self._temp_gpkg, crs_obj)
        expand = self.chk_expand_blocks.isChecked()

        if hasattr(importer, 'import_'):
            result = importer.import_(dwg_path, expand)
        elif hasattr(importer, 'importFile'):
            result = importer.importFile(dwg_path, expand)
        else:
            raise AttributeError('No import method found on QgsDwgImporter')

        if not result or not os.path.exists(self._temp_gpkg):
            raise RuntimeError('QgsDwgImporter returned error')

        self._populate_layer_list_from_gpkg(self._temp_gpkg)
        return self.tbl_layers.rowCount() > 0

    def _try_processing_import(self, input_path, crs):
        """Try Processing algorithms for DWG/DXF import."""
        import processing
        from qgis.core import QgsApplication

        crs_str = crs.authid() if crs and crs.isValid() else 'EPSG:4326'

        alg_names = [
            'native:importdwglayers',
            'native:importdxflayers',
            'qgis:importdwglayers',
            'qgis:importdxflayers',
        ]

        for alg_name in alg_names:
            alg = QgsApplication.processingRegistry().algorithmById(alg_name)
            if alg is None:
                continue

            params = {
                'INPUT': input_path,
                'TARGET_PACKAGE': self._temp_gpkg,
            }
            param_names = [p.name() for p in alg.parameterDefinitions()]

            if 'CRS' in param_names:
                params['CRS'] = crs_str
            if 'CRS_OUTPUT' in param_names:
                params['CRS_OUTPUT'] = crs_str
            if 'EXPAND_BLOCKS' in param_names:
                params['EXPAND_BLOCKS'] = self.chk_expand_blocks.isChecked()

            try:
                processing.run(alg_name, params)
                if os.path.exists(self._temp_gpkg):
                    self._populate_layer_list_from_gpkg(self._temp_gpkg)
                    if self.tbl_layers.rowCount() > 0:
                        return True
            except Exception:  # nosec B112
                continue

        return False

    def _try_load_existing_gpkg(self, dwg_path):
        """Check if there's an existing GeoPackage from previous DWG import."""
        # Check common locations where QGIS DWG import saves
        base = os.path.splitext(os.path.basename(dwg_path))[0]
        candidate_paths = [
            os.path.join(os.path.dirname(dwg_path), f'{base}.gpkg'),
            os.path.join(tempfile.gettempdir(), f'{base}.gpkg'),
        ]

        for gpkg_path in candidate_paths:
            if os.path.exists(gpkg_path):
                self._temp_gpkg = gpkg_path
                self._populate_layer_list_from_gpkg(gpkg_path)
                if self.tbl_layers.rowCount() > 0:
                    return True
        return False

    def _show_dwg_help_dialog(self, input_path, is_dwg):
        """Show help dialog when DWG cannot be read."""
        if is_dwg:
            base = os.path.splitext(os.path.basename(input_path))[0]
            gpkg_suggest = os.path.join(
                os.path.dirname(input_path), f'{base}.gpkg')

            if self.lang == 'vi':
                msg = (
                    '⚠️ Không thể đọc file DWG trực tiếp.\n\n'
                    'File DWG phiên bản mới (AutoCAD 2010+) cần '
                    'import qua QGIS:\n\n'
                    '📋 Các bước:\n'
                    '1. Vào menu: Project → Import/Export →\n'
                    '   Import Layers from DWG/DXF\n'
                    '2. Chọn file DWG của bạn\n'
                    '3. Chọn CRS (VN-2000)\n'
                    f'4. Lưu vào: {gpkg_suggest}\n'
                    '5. Nhấn Import\n\n'
                    '✅ Sau đó mở lại LVT4U DWG Converter,\n'
                    f'chọn file GPKG vừa tạo để xuất SHP.\n\n'
                    '💡 Hoặc dùng AutoCAD/LibreCAD lưu thành\n'
                    'DXF rồi mở lại với LVT4U.'
                )
            else:
                msg = (
                    '⚠️ Cannot read DWG file directly.\n\n'
                    'Modern DWG files (AutoCAD 2010+) require\n'
                    'QGIS built-in import:\n\n'
                    '📋 Steps:\n'
                    '1. Menu: Project → Import/Export →\n'
                    '   Import Layers from DWG/DXF\n'
                    '2. Select your DWG file\n'
                    '3. Choose CRS (VN-2000)\n'
                    f'4. Save to: {gpkg_suggest}\n'
                    '5. Click Import\n\n'
                    '✅ Then reopen LVT4U DWG Converter,\n'
                    f'select the created GPKG file.\n\n'
                    '💡 Or use AutoCAD/LibreCAD to save as DXF.'
                )
        else:
            msg = self._t('No layers found')

        QMessageBox.information(self, 'LVT4U', msg)

        # Offer to open QGIS DWG Import dialog directly
        if is_dwg:
            reply = QMessageBox.question(
                self, 'LVT4U',
                'Mở QGIS Import DWG ngay?\n\n'
                'Open QGIS Import DWG now?'
                if self.lang == 'vi' else
                'Open QGIS Import DWG dialog now?',
                MsgButton.Yes | MsgButton.No,
                MsgButton.Yes,
            )
            if reply == MsgButton.Yes:
                self._open_qgis_dwg_import()

    def _populate_layer_list_from_gpkg(self, gpkg_path):
        """Populate table widget from GeoPackage sublayers."""
        from ._cad_scan import LevelStat, GEOM_POINT, GEOM_LINE, GEOM_POLYGON, GEOM_NONE
        stats: dict = {}

        gpkg_layer = QgsVectorLayer(gpkg_path, 'scan', 'ogr')
        sub_layers = gpkg_layer.dataProvider().subLayers() \
            if gpkg_layer.isValid() else []

        if not sub_layers:
            # Try listing via OGR metadata
            try:
                from osgeo import ogr
                ds = ogr.Open(gpkg_path)
                if ds:
                    for i in range(ds.GetLayerCount()):
                        lyr = ds.GetLayerByIndex(i)
                        lyr_name = lyr.GetName()
                        feat_count = lyr.GetFeatureCount()
                        geom_name = ogr.GeometryTypeToName(lyr.GetGeomType())
                        stat = LevelStat(name=lyr_name)
                        kind_map = {
                            'Point': GEOM_POINT, 'MultiPoint': GEOM_POINT,
                            'Line String': GEOM_LINE, 'Multi Line String': GEOM_LINE,
                            'LineString': GEOM_LINE, 'MultiLineString': GEOM_LINE,
                            'Polygon': GEOM_POLYGON, 'Multi Polygon': GEOM_POLYGON,
                            'MultiPolygon': GEOM_POLYGON,
                        }
                        kind = kind_map.get(geom_name, GEOM_NONE)
                        stat.counts[kind] = feat_count
                        stat._source_geom = geom_name
                        stats[lyr_name] = stat
                    ds = None
            except ImportError:
                pass  # nosec B110
        else:
            for sub in sub_layers:
                parts = sub.split('!!::!!')
                if len(parts) < 4:
                    continue
                layer_name = parts[1]
                try:
                    count = int(parts[2])
                except ValueError:
                    count = 0
                geom_type = parts[3] if len(parts) > 3 else 'Unknown'
                stat = LevelStat(name=layer_name)
                kind_map = {
                    'Point': GEOM_POINT, 'MultiPoint': GEOM_POINT,
                    'LineString': GEOM_LINE, 'MultiLineString': GEOM_LINE,
                    'CompoundCurve': GEOM_LINE, 'CircularString': GEOM_LINE,
                    'Polygon': GEOM_POLYGON, 'MultiPolygon': GEOM_POLYGON,
                    'CurvePolygon': GEOM_POLYGON,
                }
                kind = kind_map.get(geom_type, GEOM_NONE)
                stat.counts[kind] = count
                stat._source_geom = geom_type
                stats[layer_name] = stat

        if gpkg_layer.isValid():
            del gpkg_layer

        self._populate_table_from_stats(stats, source='gpkg')
        self.progress.setValue(100)
        QApplication.processEvents()
        self.progress.setVisible(False)
        self.lbl_status.setText('')

        if self.tbl_layers.rowCount() == 0:
            QMessageBox.information(
                self, 'LVT4U', self._t('No layers found'))

    def _populate_table_from_stats(self, stats: dict,
                                   source: str = 'ogr',
                                   input_path: str = ''):
        """Populate QTableWidget from a dict of LevelStat objects."""
        self.tbl_layers.setRowCount(0)
        self.tbl_layers.blockSignals(True)
        self._level_stats = stats  # store for export

        # Attach source metadata to each stat for use in export
        for stat in stats.values():
            if not hasattr(stat, '_source'):
                stat._source = source
            if not hasattr(stat, '_input_path'):
                stat._input_path = input_path

        # Auto-detect encoding from text samples
        all_samples = []
        for stat in stats.values():
            all_samples.extend(stat.text_samples)
        if all_samples:
            detected = detect_encoding(all_samples)
            if detected != 'UNICODE':
                for i in range(self.cmb_encoding.count()):
                    if detected in self.cmb_encoding.itemText(i):
                        self.cmb_encoding.setCurrentIndex(i)
                        break

        row_idx = 0
        for level_name, stat in stats.items():
            if stat.total == 0:
                continue
            self.tbl_layers.insertRow(row_idx)

            # Col 0: Checkbox
            chk_item = QTableWidgetItem()
            chk_item.setFlags(QtCompat.ItemIsUserCheckable | QtCompat.ItemIsEnabled)
            chk_item.setCheckState(QtCompat.Checked)
            chk_item.setData(QtCompat.UserRole, level_name)
            self.tbl_layers.setItem(row_idx, 0, chk_item)

            # Col 1: Level name
            name_item = QTableWidgetItem(level_name)
            name_item.setData(QtCompat.UserRole, level_name)
            self.tbl_layers.setItem(row_idx, 1, name_item)

            # Col 2–5: counts by geometry type
            for col, kind in enumerate(
                    [GEOM_POINT, GEOM_LINE, GEOM_POLYGON, GEOM_TEXT],
                    start=2):
                cnt = stat.counts.get(kind, 0)
                cnt_item = QTableWidgetItem(str(cnt) if cnt > 0 else '')
                cnt_item.setTextAlignment(QtCompat.AlignCenter)
                if cnt == 0:
                    cnt_item.setForeground(QBrush(QColor('#cccccc')))

    # ------------------------------------------------------------------
    # Convert
    # ------------------------------------------------------------------

    @staticmethod
    def _safe_filename(name):
        """Clean a layer name into a safe filename."""
        for ch in ' /\\:*?"<>|':
            name = name.replace(ch, '_')
        return name

    def _convert(self):
        """Convert selected CAD layers to SHP files."""
        input_path = self.txt_input.text().strip()
        output_dir = self.txt_output.text().strip()

        if not input_path:
            QMessageBox.warning(
                self, 'LVT4U', self._t('Select source file first'))
            return
        if not output_dir:
            QMessageBox.warning(
                self, 'LVT4U', self._t('Select output folder'))
            return

        # -- Collect selected levels --
        selected_ogr_levels = []   # DXF/DGN: level names
        selected_gpkg_layers = []  # GPKG/DWG: layer dicts

        for i in range(self.tbl_layers.rowCount()):
            chk = self.tbl_layers.item(i, 0)
            if not chk or chk.checkState() != QtCompat.Checked:
                continue
            level_name = chk.data(QtCompat.UserRole)
            if not level_name or level_name not in (self._level_stats or {}):
                continue
            stat = self._level_stats[level_name]
            if stat.total == 0:
                continue
            source = getattr(stat, '_source', 'ogr')
            if source == 'gpkg':
                selected_gpkg_layers.append({
                    'name': level_name,
                    'geom_type': getattr(stat, '_source_geom', 'Unknown'),
                    'count': stat.total,
                })
            else:
                selected_ogr_levels.append(level_name)

        if not selected_ogr_levels and not selected_gpkg_layers:
            QMessageBox.warning(
                self, 'LVT4U', self._t('No layers selected'))
            return

        os.makedirs(output_dir, exist_ok=True)
        crs = self._get_selected_crs()

        # -- Encoding mode --
        enc_text = self.cmb_encoding.currentText()
        if 'TCVN3' in enc_text:
            encoding_mode = 'TCVN3'
        elif 'VNI' in enc_text:
            encoding_mode = 'VNI'
        elif 'VIQR' in enc_text:
            encoding_mode = 'VIQR'
        else:
            encoding_mode = 'UNICODE'

        extract_text_opt = self.chk_extract_text.isChecked()

        self.progress.setVisible(True)
        self.progress.setValue(0)
        self.progress.setFormat(self._t('Converting...'))
        self.lbl_status.setText(self._t('Converting...'))
        QApplication.processEvents()

        results = []

        try:
            # -- Branch A: DXF/DGN via LevelExporter --
            if selected_ogr_levels:
                from ._cad_export import export_file

                def _ogr_progress(pct, msg):
                    self.progress.setValue(max(0, min(int(pct), 99)))
                    self.progress.setFormat(msg)
                    self.lbl_status.setText(msg)
                    QApplication.processEvents()

                shp_paths = export_file(
                    file_path=input_path,
                    out_dir=output_dir,
                    target_crs=crs,
                    selected_levels=selected_ogr_levels,
                    encoding_mode=encoding_mode,
                    extract_text=extract_text_opt,
                    expand_blocks=self.chk_expand_blocks.isChecked(),
                    progress_callback=_ogr_progress,
                    level_stats=self._level_stats,
                )
                for shp_path in shp_paths:
                    if not os.path.exists(shp_path):
                        continue
                    basename = os.path.splitext(
                        os.path.basename(shp_path))[0]
                    parts = basename.rsplit('_', 1)
                    kind_label = parts[-1] if len(parts) > 1 else '?'
                    rl = QgsVectorLayer(shp_path, basename, 'ogr')
                    cnt = rl.featureCount() if rl.isValid() else 0
                    results.append((basename, kind_label, cnt, shp_path))
                    if self.chk_add_to_map.isChecked() and rl.isValid():
                        QgsProject.instance().addMapLayer(rl)
                    else:
                        del rl

            # -- Branch B: GPKG (DWG via QgsDwgImporter) --
            if selected_gpkg_layers:
                total_gpkg = len(selected_gpkg_layers)
                for idx, layer_data in enumerate(selected_gpkg_layers, 1):
                    layer_name = layer_data['name']
                    geom_type = layer_data['geom_type']
                    base_pct = int(70 + 28 * (idx - 1) / total_gpkg)
                    self.progress.setValue(base_pct)
                    self.progress.setFormat(
                        f'GPKG {idx}/{total_gpkg}: {layer_name}')
                    self.lbl_status.setText(
                        f"{self._t('Processing...')} {layer_name}")
                    QApplication.processEvents()

                    uri = f'{self._temp_gpkg}|layername={layer_name}'
                    vlayer = QgsVectorLayer(uri, layer_name, 'ogr')
                    if not vlayer.isValid() or vlayer.featureCount() == 0:
                        del vlayer
                        continue

                    safe_name = self._safe_filename(layer_name)
                    if self.chk_separate_geom.isChecked():
                        display_geom = _GEOM_MAP.get(geom_type, geom_type)
                        safe_name = f'{safe_name}_{display_geom}'
                    shp_path = os.path.join(
                        output_dir, f'{safe_name}.shp')

                    options = QgsVectorFileWriter.SaveVectorOptions()
                    options.driverName = 'ESRI Shapefile'
                    options.fileEncoding = 'UTF-8'
                    if crs and crs.isValid() and vlayer.crs().isValid():
                        if vlayer.crs().authid() != crs.authid():
                            options.ct = QgsCoordinateTransform(
                                vlayer.crs(), crs,
                                QgsProject.instance())

                    err, _, _, _ = \
                        QgsVectorFileWriter.writeAsVectorFormatV3(
                            vlayer, shp_path,
                            QgsCoordinateTransformContext(), options)
                    if err == QgsVectorFileWriter.WriterError.NoError:
                        fc = vlayer.featureCount()
                        results.append(
                            (layer_name, geom_type, fc, shp_path))
                        if self.chk_add_to_map.isChecked():
                            rl = QgsVectorLayer(shp_path, safe_name, 'ogr')
                            if rl.isValid():
                                QgsProject.instance().addMapLayer(rl)
                    del vlayer

            # -- Summary --
            self.progress.setValue(100)
            self.progress.setFormat(self._t('Conversion complete'))
            self.lbl_status.setText(self._t('Conversion complete'))
            QApplication.processEvents()

            if results:
                total_feats = sum(r[2] for r in results)
                msg = (
                    f'OK {len(results)} {self._t("layers converted")}\n'
                    f'{total_feats} {self._t("features total")}\n\n'
                    f'{output_dir}')
                for name, gtype, count, path in results:
                    display_g = _GEOM_MAP.get(gtype, gtype)
                    msg += f'\n  - {name} ({display_g}) -- {count}'
                QMessageBox.information(self, 'LVT4U', msg)
            else:
                QMessageBox.warning(
                    self, 'LVT4U', self._t('No layers found'))

        except Exception as e:
            import traceback as _tb
            QMessageBox.critical(
                self, 'LVT4U',
                f'{self._t("Error")}: {str(e)}\n\n'
                f'{_tb.format_exc()[:800]}')
        finally:
            self.progress.setVisible(False)
            self.lbl_status.setText('')
            self._cleanup_temp_gpkg()

    # ------------------------------------------------------------------
    # Cleanup
    # ------------------------------------------------------------------

    def _cleanup_temp_gpkg(self):
        """Remove temporary GeoPackage files."""
        if self._temp_gpkg:
            try:
                for ext in ['', '-wal', '-shm']:
                    p = self._temp_gpkg + ext
                    if os.path.exists(p):
                        os.remove(p)
            except OSError:
                pass  # nosec B110
            self._temp_gpkg = None

    # ------------------------------------------------------------------
    # Help
    # ------------------------------------------------------------------

    def _show_help(self):
        """Show the help dialog for this module."""
        dlg = HelpDialog('dwg_converter', self)
        dlg.exec()

    # ------------------------------------------------------------------
    # Close event
    # ------------------------------------------------------------------

    def closeEvent(self, event):
        """Clean up temp files on close."""
        self._cleanup_temp_gpkg()
        super().closeEvent(event)
